Angular2-Änderungserkennung: ngOnChanges wird nicht für verschachtelte Objekte ausgelöst

129

Ich weiß, dass ich nicht der erste bin, der danach fragt, aber ich kann in den vorherigen Fragen keine Antwort finden. Ich habe dies in einer Komponente

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

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

In der Steuerung rawLapsdatawird von Zeit zu Zeit mutiert.

In lapswerden die Daten als HTML in einem Tabellenformat ausgegeben. Dies ändert sich bei jeder rawLapsdataÄnderung.

Meine mapKomponente muss ngOnChangesals Auslöser zum Neuzeichnen von Markierungen auf einer Google Map verwendet werden. Das Problem ist, dass ngOnChanges bei rawLapsDataÄnderungen im übergeordneten Element nicht ausgelöst wird. Was kann ich tun?

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();
    }

Update: ngOnChanges funktioniert nicht, aber es sieht so aus, als würde lapsData aktualisiert. In ngInit befindet sich ein Ereignis-Listener für Zoomänderungen, der auch aufruft this.drawmarkers. Wenn ich den Zoom ändere, sehe ich tatsächlich eine Änderung der Markierungen. Das einzige Problem ist also, dass ich zum Zeitpunkt der Änderung der Eingabedaten keine Benachrichtigung erhalte.

Im Elternteil habe ich diese Zeile. (Denken Sie daran, dass sich die Änderung in Runden widerspiegelt, jedoch nicht in der Karte.)

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

Beachten Sie, dass dies this.rawLapsDataselbst ein Zeiger auf die Mitte eines großen JSON-Objekts ist

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Simon H.
quelle
Ihr Code zeigt nicht an, wie die Daten aktualisiert werden oder welcher Typ die Daten sind. Wird eine neue Instanz zugewiesen oder ist nur eine Eigenschaft des Werts geändert?
Günter Zöchbauer
@ GünterZöchbauer Ich habe die Zeile aus der übergeordneten Komponente hinzugefügt
Simon H
Ich denke, das Einwickeln dieser Zeile zone.run(...)sollte es dann tun.
Günter Zöchbauer
1
Ihr Array (Referenz) ändert sich nicht und wird daher ngOnChanges()nicht aufgerufen. Sie können ngDoCheck()Ihre eigene Logik verwenden und implementieren, um festzustellen, ob sich der Array-Inhalt geändert hat. lapsDatawird aktualisiert, weil es einen Verweis auf dasselbe Array wie hat / ist rawLapsData.
Mark Rajcok
1
1) In der Laps-Komponente durchläuft Ihr Code / Ihre Vorlage jeden Eintrag im lapsData-Array und zeigt den Inhalt an, sodass für jedes angezeigte Datenelement Winkelbindungen vorhanden sind. 2) Auch wenn Angular keine Änderungen (Referenzprüfung) an den Eingabeeigenschaften einer Komponente erkennt, werden (standardmäßig) alle Vorlagenbindungen überprüft. So nehmen Runden die Änderungen auf. 3) Die Maps-Komponente hat wahrscheinlich keine Bindungen in ihrer Vorlage zu ihrer Eingabeeigenschaft lapsData, oder? Das würde den Unterschied erklären.
Mark Rajcok

Antworten:

153

rawLapsData zeigt weiterhin auf dasselbe Array, auch wenn Sie den Inhalt des Arrays ändern (z. B. Elemente hinzufügen, Elemente entfernen, ein Element ändern).

Wenn Angular während der Änderungserkennung die Eingabeeigenschaften der Komponenten auf Änderungen überprüft, wird (im Wesentlichen) die ===Schmutzprüfung verwendet. Für Arrays bedeutet dies, dass die Array-Referenzen (nur) verschmutzt sind. Da sich die rawLapsDataArray-Referenz nicht ändert, ngOnChanges()wird sie nicht aufgerufen.

Ich kann mir zwei mögliche Lösungen vorstellen:

  1. Implementieren Sie ngDoCheck()Ihre eigene Änderungserkennungslogik und führen Sie sie aus, um festzustellen, ob sich der Array-Inhalt geändert hat. (Das Lifecycle Hooks-Dokument enthält ein Beispiel .)

  2. Weisen Sie ein neues Array zu, rawLapsDatawenn Sie Änderungen am Array-Inhalt vornehmen. Dann ngOnChanges()wird aufgerufen, da das Array (Referenz) als Änderung angezeigt wird.

In Ihrer Antwort haben Sie eine andere Lösung gefunden.

Wiederholen Sie einige Kommentare hier zum OP:

Ich sehe immer noch nicht, wie ich lapsdie Änderung aufgreifen kann (sicherlich muss sie etwas verwenden, das sich ngOnChanges()selbst entspricht?), Während ich es nicht mapkann.

  • In der lapsKomponente durchläuft Ihr Code / Ihre Vorlage jeden Eintrag im lapsDataArray und zeigt den Inhalt an, sodass für jedes angezeigte Datenelement Winkelbindungen vorhanden sind.
  • Selbst wenn Angular keine Änderungen an den Eingabeeigenschaften einer Komponente erkennt (mithilfe der ===Überprüfung), werden alle Vorlagenbindungen (standardmäßig) überprüft. Wenn sich eine dieser Änderungen ändert, aktualisiert Angular das DOM. Das sehen Sie.
  • Die mapsKomponente hat wahrscheinlich keine Bindungen in ihrer Vorlage zu ihrer lapsDataEingabeeigenschaft, oder? Das würde den Unterschied erklären.

Beachten Sie, dass lapsDatain beiden Komponenten und rawLapsDatain der übergeordneten Komponente alle auf dasselbe / ein Array verweisen. Obwohl Angular keine (Referenz-) Änderungen an den lapsDataEingabeeigenschaften bemerkt , "erhalten" / sehen die Komponenten Änderungen des Array-Inhalts, da sie alle dieses eine Array gemeinsam nutzen / referenzieren. Wir brauchen Angular nicht, um diese Änderungen zu verbreiten, wie wir es bei einem primitiven Typ (Zeichenfolge, Zahl, Boolescher Wert) tun würden. Bei einem primitiven Typ würde jedoch jede Änderung des Werts immer ausgelöst ngOnChanges()- was Sie in Ihrer Antwort / Lösung ausnutzen.

Wie Sie wahrscheinlich inzwischen herausgefunden haben, verhalten sich Objekteingabeeigenschaften genauso wie Array-Eingabeeigenschaften.

Mark Rajcok
quelle
Ja, ich habe ein tief verschachteltes Objekt mutiert, und ich denke, das hat es Angular schwer gemacht, Änderungen in der Struktur zu erkennen. Nicht meine Vorliebe für Mutation, aber dies ist übersetztes XML und ich kann es mir nicht leisten, die umgebenden Daten zu verlieren, da ich die XML am Ende erneut erstellen möchte
Simon H
7
@SimonH, "für Angular ist es schwierig, Änderungen in der Struktur zu erkennen" - Um ganz klar zu sein, Angular schaut nicht einmal in Eingabeeigenschaften, die Arrays oder Objekte für Änderungen sind. Es wird nur geprüft, ob sich der Wert geändert hat. Bei Objekten und Arrays ist der Wert die Referenz. Bei primitiven Typen ist der Wert ... der Wert. (Ich bin nicht sicher, ob ich den ganzen Jargon richtig verstanden habe, aber Sie haben die Idee.)
Mark Rajcok
11
Gute Antwort. Das Angular2-Team muss dringend ein detailliertes, maßgebliches Dokument zu den Interna der Änderungserkennung veröffentlichen.
Wenn ich die Funktionalität in doCheck ausführe, wird in meinem Fall die do-Prüfung so oft aufgerufen. Kannst du mir bitte etwas anderes sagen?
Mr_Perfect
@ MarkRajcok können Sie mir bitte helfen, dieses Problem zu lösen stackoverflow.com/questions/50166996/…
Nikson
27

Nicht der sauberste Ansatz, aber Sie können das Objekt jedes Mal klonen, wenn Sie den Wert ändern?

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

Ich denke, ich würde diesen Ansatz der Implementierung Ihres eigenen vorziehen, ngDoCheck()aber vielleicht könnte sich jemand wie @ GünterZöchbauer einschalten.

David
quelle
Wenn Sie nicht sicher sind, ob ein Zielbrowser <Object.assign ()> unterstützen würde, können Sie auch eine JSON-Zeichenfolge erstellen und zurück zu JSON analysieren, wodurch auch ein neues Objekt erstellt wird ...
Guntram
1
@ Guntram Oder eine Polyfüllung?
David
12

Bei Arrays können Sie dies folgendermaßen tun:

In der .tsDatei (übergeordnete Komponente), in der Sie Ihr Update durchführen, rawLapsDatagehen Sie wie folgt vor:

rawLapsData = somevalue; // change detection will not happen

Lösung:

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

und ngOnChangeswird in der untergeordneten Komponente aufgerufen

Dänischer Dullu
quelle
10

Als Erweiterung von Mark Rajcoks zweiter Lösung

Weisen Sie rawLapsData ein neues Array zu, wenn Sie Änderungen am Array-Inhalt vornehmen. Dann wird ngOnChanges () aufgerufen, da das Array (die Referenz) als Änderung angezeigt wird

Sie können den Inhalt des Arrays folgendermaßen klonen:

rawLapsData = rawLapsData.slice(0);

Ich erwähne das, weil

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

hat bei mir nicht funktioniert. Ich hoffe das hilft.

decebal
quelle
8

Wenn die Daten aus einer externen Bibliothek stammen, müssen Sie möglicherweise die Anweisung data upate in ausführen zone.run(...). Injizieren Sie zone: NgZonedafür. Wenn Sie die Instanziierung der externen Bibliothek zone.run()bereits ausführen können , benötigen Sie diese möglicherweise zone.run()später nicht mehr.

Günter Zöchbauer
quelle
Wie in den Kommentaren zum OP erwähnt, waren die Änderungen nicht extern, sondern tief in einem JSON-Objekt
Simon H
1
Wie Ihre Antwort sagt, müssen wir noch etwas ausführen, um die Dinge in Angular2 synchron zu halten, wie es Angular1 war $scope.$apply?
Pankaj Parkar
1
Wenn etwas von außerhalb von Angular gestartet wird, wird die nach Zone gepatchte API nicht verwendet und Angular wird nicht über mögliche Änderungen benachrichtigt. Ja, zone.runist ähnlich wie $scope.apply.
Günter Zöchbauer
6

Verwenden ChangeDetectorRef.detectChanges()Sie diese Option, um Angular anzuweisen, eine Änderungserkennung auszuführen, wenn Sie ein verschachteltes Objekt bearbeiten (das bei der fehlerhaften Überprüfung fehlt).

Tim Phillips
quelle
Aber wie ? Ich versuche dies zu verwenden. Ich möchte changeDetection in einem Kind auslösen, nachdem ein Elternteil ein neues Element auf eine 2-Wege-Grenze [(Sammlung)] verschoben hat.
Deunz
Das Problem ist nicht, dass keine Änderungserkennung stattfindet, sondern dass die Änderungserkennung die Änderungen nicht erkennt! Das scheint also keine Lösung zu sein! Warum hat diese Antwort fünf positive Stimmen bekommen?
Shahryar Saljoughi
5

Ich habe 2 Lösungen, um Ihr Problem zu lösen

  1. Verwenden Sie ngDoCheckdiese Option, um objectgeänderte oder nicht geänderte Daten zu erkennen
  2. Weisen Sie objecteiner neuen Speicheradresse von object = Object.create(object)der übergeordneten Komponente zu.
giapnh
quelle
2
Gibt es einen bemerkenswerten Unterschied zwischen Object.create (Objekt) und Object.assign ({}, Objekt)?
Daniel Galarza
3

Meine "Hack" -Lösung ist

   <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 ändert sich gleichzeitig mit rawLapsData, und dies gibt map eine weitere Möglichkeit, die Änderung durch einen einfacheren primitiven Objekttyp zu erkennen . Es ist NICHT elegant, aber es funktioniert.

Simon H.
quelle
Es fällt mir schwer, alle Änderungen an verschiedenen Komponenten in der Vorlagensyntax zu verfolgen, insbesondere an mittelgroßen / großen Apps. Normalerweise verwende ich Shared Event Emitter und Abonnement, um die Daten zu übergeben (schnelle Lösung), oder implementiere dafür ein Redux-Muster (über Rx.Subject) (wenn Zeit zum Planen ist) ...
Sasxa
3

Die Änderungserkennung wird nicht ausgelöst, wenn Sie eine Eigenschaft eines Objekts (einschließlich eines verschachtelten Objekts) ändern. Eine Lösung wäre, eine neue Objektreferenz mit der Funktion 'lodash' clone () neu zuzuweisen.

import * as _ from 'lodash';

this.foo = _.clone(this.foo);
Anton Nikprelaj
quelle
2

Hier ist ein Hack, der mich aus diesem Problem herausgeholt hat.

Ein ähnliches Szenario wie beim OP: Ich habe eine verschachtelte Angular-Komponente, an die Daten weitergegeben werden müssen, aber die Eingabe zeigt auf ein Array. Wie oben erwähnt, sieht Angular keine Änderung, da die nicht untersucht wird Inhalt des Arrays.

Um dies zu beheben, konvertiere ich das Array in eine Zeichenfolge, damit Angular eine Änderung erkennt, und teile dann in der verschachtelten Komponente die Zeichenfolge wieder in ein Array und seine glücklichen Tage auf (',').

Graham Phillips
quelle
1
Yuck! Der Ansatz array.slice (0) ist viel sauberer.
Chris Haines
2

Ich bin auf das gleiche Bedürfnis gestoßen. Und ich habe viel darüber gelesen, hier ist mein Kupfer zu diesem Thema.

Wenn Sie möchten, dass Ihre Änderungserkennung auf Push erfolgt, haben Sie sie dann, wenn Sie den Wert eines Objekts im Inneren ändern, oder? Und Sie hätten es auch, wenn Sie irgendwie Objekte entfernen würden.

Wie bereits erwähnt, verwenden Sie changeDetectionStrategy.onPush

Angenommen, Sie haben diese Komponente mit changeDetectionStrategy.onPush erstellt:

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

Dann würden Sie einen Artikel drücken und die Änderungserkennung auslösen:

myCollection.push(anItem);
refresh();

oder Sie entfernen ein Element und lösen die Änderungserkennung aus:

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

oder Sie ändern einen Attributwert für ein Element und lösen die Änderungserkennung aus:

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

Inhalt der Aktualisierung:

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

Die Slice-Methode gibt genau dasselbe Array zurück, und das Zeichen [=] verweist neu darauf und löst die Änderungserkennung jedes Mal aus, wenn Sie sie benötigen. Einfach und lesbar :)

Grüße,

Deunz
quelle
2

Ich habe alle hier genannten Lösungen ausprobiert, aber aus irgendeinem Grund ngOnChanges()immer noch nicht für mich ausgelöst. Also habe ich es mit aufgerufen, this.ngOnChanges()nachdem ich den Dienst angerufen hatte, der meine Arrays neu bevölkert hat, und es hat funktioniert ... richtig? wahrscheinlich nicht. Ordentlich? Auf keinen Fall. Funktioniert? Ja!

Yazan Khalaileh
quelle
1

ok also meine lösung dafür war:

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

Und das löst mich ngOnChanges aus

DanielWaw
quelle
0

Ich musste einen Hack dafür erstellen -

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

In meinem Fall waren es Änderungen des Objektwerts, die ngOnChangenicht erfasst wurden. Einige Objektwerte werden als Reaktion auf einen API-Aufruf geändert. Durch die Neuinitialisierung des Objekts wurde das Problem behoben und das ngOnChangeAuslösen in der untergeordneten Komponente verursacht.

Etwas wie

 this.pagingObj = new Paging(); //This line did the magic
 this.pagingObj.pageNumber = response.PageNumber;
Kailas
quelle