Rilevamento modifiche Angular2: ngOnChanges non si attiva per l'oggetto nidificato


129

So di non essere il primo a chiederlo, ma non riesco a trovare una risposta nelle domande precedenti. Ho questo in un componente

<div class="col-sm-5">
    <laps
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"
        (lapsHandler)="lapsHandler($event)">
    </laps>
</div>

<map
    [lapsData]="rawLapsData"
    class="col-sm-7">
</map>

Nel controller rawLapsdataviene mutato di volta in volta.

In laps, i dati vengono emessi come HTML in un formato tabulare. Questo cambia ogni volta che rawLapsdatacambia.

Il mio mapcomponente deve essere utilizzato ngOnChangescome trigger per ridisegnare i marker su una mappa di Google. Il problema è che ngOnChanges non si attiva quando si verificano rawLapsDatamodifiche nel genitore. Cosa posso fare?

import {Component, Input, OnInit, OnChanges, SimpleChange} from 'angular2/core';

@Component({
    selector: 'map',
    templateUrl: './components/edMap/edMap.html',
    styleUrls: ['./components/edMap/edMap.css']
})
export class MapCmp implements OnInit, OnChanges {
    @Input() lapsData: any;
    map: google.maps.Map;

    ngOnInit() {
        ...
    }

    ngOnChanges(changes: { [propName: string]: SimpleChange }) {
        console.log('ngOnChanges = ', changes['lapsData']);
        if (this.map) this.drawMarkers();
    }

Aggiornamento: ngOnChanges non funziona, ma sembra che lapsData sia in fase di aggiornamento. In ngInit è presente un listener di eventi per le modifiche dello zoom che chiama anche this.drawmarkers. Quando cambio lo zoom vedo davvero un cambiamento nei marker. Quindi l'unico problema è che non ricevo la notifica al momento della modifica dei dati di input.

Nel genitore, ho questa linea. (Ricorda che la modifica si riflette nei giri, ma non nella mappa).

this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);

E nota che this.rawLapsDataè esso stesso un puntatore al centro di un grande oggetto json

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;

Il tuo codice non mostra come vengono aggiornati i dati o che tipo di dati sono. Viene assegnata una nuova istanza o viene modificata solo una proprietà del valore?
Günter Zöchbauer,

@ GünterZöchbauer Ho aggiunto la linea dal componente padre
Simon H

Immagino che avvolgere questa linea zone.run(...)dovrebbe farlo allora.
Günter Zöchbauer,

1
L'array (riferimento) non sta cambiando, quindi ngOnChanges()non verrà chiamato. È possibile utilizzare ngDoCheck()e implementare la propria logica per determinare se il contenuto dell'array è stato modificato. lapsDataviene aggiornato perché ha / è un riferimento allo stesso array di rawLapsData.
Mark Rajcok,

1
1) Nel componente giri il tuo codice / modello scorre su ogni voce dell'array lapsData e visualizza i contenuti, quindi ci sono collegamenti angolari su ogni pezzo di dati che viene visualizzato. 2) Anche se Angular non rileva alcuna modifica (controllo di riferimento) alle proprietà di input di un componente, controlla comunque (per impostazione predefinita) tutti i binding del modello. È così che i giri raccolgono le modifiche. 3) Il componente delle mappe probabilmente non ha alcun legame nel suo modello con la sua proprietà di input lapsData, giusto? Ciò spiegherebbe la differenza.
Mark Rajcok,

Risposte:


153

rawLapsData continua a puntare allo stesso array, anche se si modifica il contenuto dell'array (ad esempio, aggiungere elementi, rimuovere elementi, cambiare un elemento).

Durante il rilevamento delle modifiche, quando Angular controlla le proprietà di input dei componenti per la modifica, utilizza (essenzialmente) ===per il controllo sporco. Per le matrici, ciò significa che i riferimenti di matrice (solo) sono sottoposti a verifica sporca. Poiché il rawLapsDatariferimento dell'array non sta cambiando, ngOnChanges()non verrà chiamato.

Mi vengono in mente due possibili soluzioni:

  1. Implementare ngDoCheck()ed eseguire la propria logica di rilevamento delle modifiche per determinare se il contenuto dell'array è cambiato. (Il documento Ganci del ciclo di vita ha un esempio .)

  2. Assegnare un nuovo array rawLapsDataogni volta che si apportano modifiche al contenuto dell'array. Quindi ngOnChanges()verrà chiamato perché l'array (riferimento) verrà visualizzato come una modifica.

Nella tua risposta, hai trovato un'altra soluzione.

Ripetendo alcuni commenti qui sul PO:

Non riesco ancora a vedere come si lapspuò capire il cambiamento (sicuramente deve usare qualcosa di equivalente a ngOnChanges()se stesso?) Mentre mapnon ci riesco.

  • Nel lapscomponente il codice / modello scorre su ogni voce lapsDatadell'array e visualizza i contenuti, quindi ci sono associazioni angolari su ogni pezzo di dati che viene visualizzato.
  • Anche quando Angular non rileva alcuna modifica alle proprietà di input di un componente (usando il ===controllo), controlla comunque (di default) tutte le associazioni dei template. Quando uno di questi cambia, Angular aggiornerà il DOM. Questo è quello che stai vedendo.
  • Il mapscomponente probabilmente non ha alcun legame nel suo modello con la sua lapsDataproprietà di input, giusto? Ciò spiegherebbe la differenza.

Si noti che lapsDatain entrambi i componenti e rawLapsDatanel componente padre tutti puntano allo stesso / un array. Quindi, anche se Angular non nota alcuna modifica (di riferimento) alle lapsDataproprietà di input, i componenti "ottengono" / vedono eventuali modifiche al contenuto dell'array perché condividono / fanno riferimento a quell'array. Non abbiamo bisogno di Angular per propagare questi cambiamenti, come faremmo con un tipo primitivo (stringa, numero, booleano). Ma con un tipo primitivo, qualsiasi modifica al valore si innescherebbe sempre ngOnChanges()- che è qualcosa che sfrutti nella tua risposta / soluzione.

Come probabilmente hai già capito, le proprietà di input dell'oggetto hanno lo stesso comportamento delle proprietà di input dell'array.


Sì, stavo mutando un oggetto profondamente annidato e immagino che ciò rendesse difficile per Angular individuare i cambiamenti nella struttura. Non è la mia preferenza per la mutazione, ma questo è XML tradotto e non posso permettermi di perdere nessuno dei dati circostanti poiché voglio ricreare nuovamente l'xml alla fine
Simon H,

7
@SimonH, "difficile per Angular individuare i cambiamenti nella struttura" - Per essere chiari, Angular non guarda nemmeno all'interno delle proprietà di input che sono array o oggetti per le modifiche. Sembra solo vedere se il valore è cambiato - per oggetti e matrici, il valore è il riferimento. Per i tipi primitivi, il valore è ... il valore. (Non sono sicuro di avere tutto il gergo dritto, ma hai
capito

11
Bella risposta. Il team di Angular2 deve disperatamente pubblicare un documento dettagliato e autorevole sugli interni del rilevamento delle modifiche.

Se faccio la funzionalità in doCheck, nel mio caso il do check sta chiamando così tante volte. Puoi dirmelo in qualsiasi altro modo?
Mr_Perfect il

@MarkRajcok si può piacere aiutarmi a risolvere questo problema stackoverflow.com/questions/50166996/...
Nikson

27

Non è l'approccio più pulito, ma puoi semplicemente clonare l'oggetto ogni volta che cambi il valore?

   rawLapsData = Object.assign({}, rawLapsData);

Penso che preferirei questo approccio ngDoCheck()piuttosto che implementare il tuo, ma forse qualcuno come @ GünterZöchbauer potrebbe intervenire.


Se non si è sicuri che un browser di destinazione supporti <Object.assign ()>, è anche possibile eseguire la stringa in una stringa json e analizzare nuovamente json, che creerà anche un nuovo oggetto ...
Guntram

1
@Guntram O un polyfill?
David,

12

In Case of Arrays puoi farlo in questo modo:

Nel .tsfile (componente principale) in cui stai aggiornando, rawLapsDatafallo in questo modo:

rawLapsData = somevalue; // change detection will not happen

Soluzione:

rawLapsData = {...somevalue}; //change detection will happen

e ngOnChangeschiamerà nel componente figlio


10

Come estensione della seconda soluzione di Mark Rajcok

Assegnare un nuovo array a rawLapsData ogni volta che si apportano modifiche al contenuto dell'array. Quindi verrà chiamato ngOnChanges () perché l'array (riferimento) verrà visualizzato come una modifica

puoi clonare il contenuto dell'array in questo modo:

rawLapsData = rawLapsData.slice(0);

Sto citando questo perché

rawLapsData = Object.assign ({}, rawLapsData);

non ha funzionato per me. Spero che aiuti.


8

Se i dati provengono da una libreria esterna, potrebbe essere necessario eseguire l'istruzione di aggiornamento dei dati all'interno zone.run(...). Iniettare zone: NgZoneper questo. Se è già possibile eseguire l'istanza della libreria esterna all'interno zone.run(), potrebbe non essere necessario in un zone.run()secondo momento.


Come notato nei commenti al PO, le modifiche non furono esterne ma profonde all'interno di un oggetto json
Simon H

1
Come dice la tua risposta, dobbiamo ancora eseguire qualcosa per mantenere le cose in sincronia in Angular2, come era angular1 $scope.$apply?
Pankaj Parkar,

1
Se qualcosa viene avviato dall'esterno di Angular, l'API, patchata per zona non viene utilizzata e Angular non viene avvisato di possibili modifiche. Sì, zone.runè simile a $scope.apply.
Günter Zöchbauer,

6

Utilizzare ChangeDetectorRef.detectChanges()per dire ad Angular di eseguire un rilevamento delle modifiche quando si modifica un oggetto nidificato (che manca con il suo controllo sporco).


Ma come ? Sto provando a usare questo, voglio innescare changeDetection in un bambino dopo che il genitore ha spinto un nuovo elemento in un modo limitato a 2 ((Collection)].
Deunz,

Il problema non è che il rilevamento delle modifiche non si verifica, ma che il rilevamento delle modifiche non rileva le modifiche! quindi questa non sembra essere una soluzione! perché questa risposta ha ottenuto cinque voti?
Shahryar Saljoughi,

5

Ho 2 soluzioni per risolvere il tuo problema

  1. Utilizzare ngDoCheckper rilevare i objectdati modificati o meno
  2. Assegna objecta un nuovo indirizzo di memoria object = Object.create(object)dal componente principale.

2
Ci sarebbe una notevole differenza tra Object.create (oggetto) rispetto a Object.assign ({}, oggetto)?
Daniel Galarza,

3

La mia soluzione "hack" è

   <div class="col-sm-5">
        <laps
            [lapsData]="rawLapsData"
            [selectedTps]="selectedTps"
            (lapsHandler)="lapsHandler($event)">
        </laps>
    </div>
    <map
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"   // <--------
        class="col-sm-7">
    </map>

selectedTps cambia contemporaneamente a rawLapsData e ciò offre alla mappa un'altra possibilità di rilevare la modifica attraverso un tipo primitivo di oggetto più semplice . NON è elegante, ma funziona.


Trovo difficile tenere traccia di tutte le modifiche su vari componenti nella sintassi del modello, specialmente su app di medie / grandi dimensioni. Di solito uso l'emettitore di eventi condiviso e l'abbonamento per passare i dati (soluzione rapida), o implemento il modello Redux (tramite Rx.Subject) per questo (quando c'è tempo per pianificare) ...
Sasxa

3

Il rilevamento delle modifiche non viene attivato quando si modifica una proprietà di un oggetto (incluso l'oggetto nidificato). Una soluzione sarebbe quella di riassegnare un nuovo riferimento all'oggetto usando la funzione clone () 'lodash'.

import * as _ from 'lodash';

this.foo = _.clone(this.foo);

2

Ecco un trucco che mi ha appena messo nei guai con questo.

Quindi uno scenario simile all'OP: ho un componente angolare nidificato che necessita di dati passati ad esso, ma l'input punta a un array e, come menzionato sopra, Angular non vede un cambiamento in quanto non esamina il contenuto dell'array.

Quindi, per risolverlo, converto l'array in una stringa affinché Angular rilevi una modifica, quindi nel componente nidificato ho diviso (',') nuovamente la stringa in un array e i suoi giorni felici.


1
Che schifo! L'approccio array.slice (0) è molto più pulito.
Chris Haines,

2

Mi sono imbattuto nello stesso bisogno. E ho letto molto su questo, quindi, ecco il mio rame sull'argomento.

Se desideri che il rilevamento delle modifiche venga trasmesso, lo avresti quando cambi un valore di un oggetto all'interno giusto? E lo avresti anche se in qualche modo rimuovessi gli oggetti.

Come già detto, utilizzare changeDetectionStrategy.onPush

Supponi di avere questo componente che hai creato, con changeDetectionStrategy.onPush:

<component [collection]="myCollection"></component>

Quindi spingi un elemento e attivi il rilevamento delle modifiche:

myCollection.push(anItem);
refresh();

oppure rimuovere un elemento e attivare il rilevamento delle modifiche:

myCollection.splice(0,1);
refresh();

o cambieresti un valore attrbibute per un oggetto e attiveresti il ​​rilevamento delle modifiche:

myCollection[5].attribute = 'new value';
refresh();

Contenuto di aggiornamento:

refresh() : void {
    this.myCollection = this.myCollection.slice();
}

Il metodo slice restituisce esattamente lo stesso array e il segno [=] ne fa un nuovo riferimento, attivando il rilevamento delle modifiche ogni volta che ne hai bisogno. Facile e leggibile :)

Saluti,


2

Ho provato tutte le soluzioni menzionate qui, ma per qualche motivo ngOnChanges()ancora non ha funzionato per me. Così l'ho chiamato this.ngOnChanges()dopo aver chiamato il servizio che ripopola le mie matrici e ha funzionato .... giusto? probabilmente no. Neat? diavolo, no. Lavori? sì!


1

ok quindi la mia soluzione per questo era:

this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;

E questo mi fa innescare ngOnChanges


0

Ho dovuto creare un trucco per questo -

I created a Boolean Input variable and toggled it whenever array changed, which triggered change detection in the child component, hence achieving the purpose

0

Nel mio caso sono state le modifiche nel valore dell'oggetto che ngOnChangenon è stato acquisito. Alcuni valori di oggetto vengono modificati in risposta alla chiamata api. La reinizializzazione dell'oggetto ha risolto il problema e provocato l' ngOnChangeattivazione nel componente figlio.

Qualcosa di simile a

 this.pagingObj = new Paging(); //This line did the magic
 this.pagingObj.pageNumber = response.PageNumber;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.