Winkel 2 - Ansicht wird nach Modelländerungen nicht aktualisiert

128

Ich habe eine einfache Komponente, die alle paar Sekunden eine REST-API aufruft und einige JSON-Daten zurückerhält. Ich kann anhand meiner Protokollanweisungen und des Netzwerkverkehrs erkennen, dass sich die zurückgegebenen JSON-Daten ändern und mein Modell aktualisiert wird. Die Ansicht ändert sich jedoch nicht.

Meine Komponente sieht aus wie:

import {Component, OnInit} from 'angular2/core';
import {RecentDetectionService} from '../services/recentdetection.service';
import {RecentDetection} from '../model/recentdetection';
import {Observable} from 'rxjs/Rx';

@Component({
    selector: 'recent-detections',
    templateUrl: '/app/components/recentdetection.template.html',
    providers: [RecentDetectionService]
})



export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { this.recentDetections = recent;
             console.log(this.recentDetections[0].macAddress) });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Und meine Ansicht sieht aus wie:

<div class="panel panel-default">
    <!-- Default panel contents -->
    <div class="panel-heading"><h3>Recently detected</h3></div>
    <div class="panel-body">
        <p>Recently detected devices</p>
    </div>

    <!-- Table -->
    <table class="table" style="table-layout: fixed;  word-wrap: break-word;">
        <thead>
            <tr>
                <th>Id</th>
                <th>Vendor</th>
                <th>Time</th>
                <th>Mac</th>
            </tr>
        </thead>
        <tbody  >
            <tr *ngFor="#detected of recentDetections">
                <td>{{detected.broadcastId}}</td>
                <td>{{detected.vendor}}</td>
                <td>{{detected.timeStamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
                <td>{{detected.macAddress}}</td>
            </tr>
        </tbody>
    </table>
</div>

Ich kann anhand der Ergebnisse sehen, console.log(this.recentDetections[0].macAddress)dass das RecentDetections-Objekt aktualisiert wird, aber die Tabelle in der Ansicht ändert sich nie, es sei denn, ich lade die Seite neu.

Ich kämpfe darum zu sehen, was ich hier falsch mache. Kann jemand helfen?

Matt Watson
quelle
Ich empfehle, Code sauberer und weniger komplex zu machen: stackoverflow.com/a/48873980/571030
glukki

Antworten:

214

Es kann sein, dass der Code in Ihrem Dienst irgendwie aus Angulars Zone ausbricht. Dies unterbricht die Änderungserkennung. Das sollte funktionieren:

import {Component, OnInit, NgZone} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private zone:NgZone, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                 this.zone.run(() => { // <== added
                     this.recentDetections = recent;
                     console.log(this.recentDetections[0].macAddress) 
                 });
        });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Weitere Möglichkeiten zum Aufrufen der Änderungserkennung finden Sie unter Manuelles Auslösen der Änderungserkennung in Angular

Alternative Möglichkeiten zum Aufrufen der Änderungserkennung sind

ChangeDetectorRef.detectChanges()

um die Änderungserkennung für die aktuelle Komponente und ihre untergeordneten Komponenten sofort auszuführen

ChangeDetectorRef.markForCheck()

um die aktuelle Komponente beim nächsten Mal einzubeziehen

ApplicationRef.tick()

um die Änderungserkennung für die gesamte Anwendung auszuführen

Günter Zöchbauer
quelle
9
Eine andere Alternative besteht darin, ChangeDetectorRef zu injizieren und cdRef.detectChanges()stattdessen aufzurufen zone.run(). Dies könnte effizienter sein, da die Änderungserkennung nicht wie im gesamten Komponentenbaum ausgeführt zone.run()wird.
Mark Rajcok
1
Zustimmen. Nur verwenden, detectChanges()wenn Änderungen, die Sie vornehmen, lokal für die Komponente und ihre Nachkommen sind. Mein Verständnis ist , dass detectChanges()die Komponente und ihre Nachkommen überprüfen, sondern zone.run()und ApplicationRef.tick()den gesamten Komponentenbaum überprüfen.
Mark Rajcok
2
Genau das, wonach ich gesucht habe. Die Verwendung @Input()ergab weder Sinn noch funktionierte sie.
yorch
15
Ich verstehe wirklich nicht, warum wir all dieses manuelle Zeug brauchen oder warum eckige Götter so notwendig sind. Warum bedeutet @Input nicht, dass die Komponente wirklich von dem abhängt, was die übergeordnete Komponente in ihren sich ändernden Daten gesagt hat? Es scheint äußerst unelegant, dass es nicht nur funktioniert. Was vermisse ich?
13
Hat für mich gearbeitet. Dies ist jedoch ein weiteres Beispiel, warum ich das Gefühl habe, dass Entwickler von Winkelgerüsten in der Komplexität des Winkelgerüsts verloren sind. Wenn Sie Angular als Anwendungsentwickler verwenden, müssen Sie so viel Zeit aufwenden, um zu verstehen, wie Angular dies oder das tut und wie Sie die oben beschriebenen Probleme umgehen können. Schrecklich !!
Karl
32

Es ist ursprünglich eine Antwort in den Kommentaren von @Mark Rajcok, aber ich möchte es hier als getestete und als Lösung mit ChangeDetectorRef funktionierende Lösung platzieren . Ich sehe hier einen guten Punkt:

Eine weitere Alternative ist zu injizieren ChangeDetectorRefund rufen cdRef.detectChanges()statt zone.run(). Dies könnte effizienter sein, da die Änderungserkennung nicht wie im gesamten Komponentenbaum ausgeführt zone.run()wird. - Mark Rajcok

Code muss also wie folgt aussehen:

import {Component, OnInit, ChangeDetectorRef} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Bearbeiten : Die Verwendung eines .detectChanges()internen Subscibe kann zu Problemen führen. Versuch, eine zerstörte Ansicht zu verwenden: detectChanges

Um es zu lösen, müssen Sie unsubscribevor dem Zerstören der Komponente Folgendes tun: Der vollständige Code lautet wie folgt:

import {Component, OnInit, ChangeDetectorRef, OnDestroy} from 'angular2/core';

export class RecentDetectionComponent implements OnInit, OnDestroy {

    recentDetections: Array<RecentDetection>;
    private timerObserver: Subscription;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        this.timerObserver = timer.subscribe(() => this.getRecentDetections());
    }

    ngOnDestroy() {
        this.timerObserver.unsubscribe();
    }

}
Al-Mothafar
quelle
this.cdRef.detectChanges (); mein Problem behoben. Danke!
Mangesh
Vielen Dank für Ihren Vorschlag, aber nach mehreren Aufrufversuchen wird folgende Fehlermeldung angezeigt: Fehler: Fehler: ViewDestroyedError: Versuch, eine zerstörte Ansicht zu verwenden: detectChanges Für mich hilft zone.run (), diesen Fehler zu beheben.
Shivam Srivastava
8

Versuchen zu benutzen @Input() recentDetections: Array<RecentDetection>;

BEARBEITEN: Der Grund dafür@Input()ist, dass Sie den Wert in der Typoskript- / Javascript-Datei an die Ansicht (HTML) binden möchten. Die Ansicht aktualisiert sich selbst, wenn ein mit dem@Input()Dekoratordeklarierter Wertgeändert wird. Wenn ein@Input()oder@Output()Dekorator geändert wird, wird einngOnChanges-event ausgelöst und die Ansicht wird mit dem neuen Wert aktualisiert. Sie können sagen, dass der@Input()Wille den Wert in beide Richtungen bindet.

Suchen Sie nach Eingabe auf diesem Link nach Winkel: Glossar für weitere Informationen

BEARBEITEN: Nachdem ich mehr über die Entwicklung von Angular 2 erfahren hatte, stellte ich fest, dass@Input()es nicht die Lösung ist, wirklich etwas zutun, und wie in den Kommentaren erwähnt,

@Input() Dies gilt nur, wenn die Daten durch Datenbindung von außerhalb der Komponente geändert werden (gebundene Daten im übergeordneten Element wurden geändert), nicht, wenn die Daten vom Code innerhalb der Komponente geändert werden.

Wenn Sie sich die Antwort von @ Günter ansehen, ist dies eine genauere und korrektere Lösung des Problems. Ich werde diese Antwort hier behalten, aber bitte folge Günters Antwort als die richtige.

John
quelle
2
Das war's. Ich hatte die Annotation @Input () schon einmal gesehen, sie aber immer mit Formulareingaben verknüpft, hätte nie gedacht, dass ich sie hier brauchen würde. Prost.
Matt Watson
1
@ John danke für die zusätzliche Erklärung. Was Sie hinzugefügt haben, gilt jedoch nur, wenn die Daten durch Datenbindung von außerhalb der Komponente geändert werden (gebundene Daten im übergeordneten Element geändert), nicht, wenn die Daten vom Code innerhalb der Komponente geändert werden.
Günter Zöchbauer
Bei Verwendung des @Input()Schlüsselworts ist eine Eigenschaftsbindung angehängt , und diese Bindung stellt sicher, dass der Wert in der Ansicht aktualisiert wird. Der Wert ist Teil eines Lebenszyklusereignisses. Dieser Beitrag enthält einige weitere Informationen über das @Input()Schlüsselwort
John
Ich werde aufgeben. Ich sehe nicht, wo ein @Input()hier involviert ist oder warum es sein sollte und die Frage liefert nicht genug Informationen.
Günter Zöchbauer
3
@Input()ist eher eine Problemumgehung, die die Wurzel des Problems verbirgt. Die Probleme müssen mit den Zonen und der Änderungserkennung zusammenhängen.
Magiccrafter
2

In meinem Fall hatte ich ein sehr ähnliches Problem. Ich habe meine Ansicht in einer Funktion aktualisiert, die von einer übergeordneten Komponente aufgerufen wurde, und in meiner übergeordneten Komponente habe ich vergessen, @ViewChild (NameOfMyChieldComponent) zu verwenden. Ich habe mindestens 3 Stunden nur für diesen dummen Fehler verloren. dh: Ich musste keine dieser Methoden anwenden:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
user3674783
quelle
1
Können Sie erklären, wie Sie Ihre untergeordnete Komponente mit @ViewChild aktualisiert haben? danke
Lucas Colombo
Ich vermute, Eltern haben eine Methode einer
Kinderklasse aufgerufen
1

Anstatt sich mit Zonen und Änderungserkennung zu befassen, lassen Sie AsyncPipe die Komplexität bewältigen. Dadurch werden beobachtbare Abonnements, Abbestellungen (um Speicherlecks zu vermeiden) und Änderungserkennung auf Angular-Schultern ausgeführt.

Ändern Sie Ihre Klasse, um eine beobachtbare Klasse zu erstellen, die Ergebnisse neuer Anforderungen ausgibt:

export class RecentDetectionComponent implements OnInit {

    recentDetections$: Observable<Array<RecentDetection>>;

    constructor(private recentDetectionService: RecentDetectionService) {
    }

    ngOnInit() {
        this.recentDetections$ = Observable.interval(5000)
            .exhaustMap(() => this.recentDetectionService.getJsonFromApi())
            .do(recent => console.log(recent[0].macAddress));
    }
}

Und aktualisieren Sie Ihre Ansicht, um AsyncPipe zu verwenden:

<tr *ngFor="let detected of recentDetections$ | async">
    ...
</tr>

Möchten Sie hinzufügen, dass es besser ist, einen Dienst mit einer Methode zu erstellen, die intervalArgumente akzeptiert, und:

  • neue Anforderungen erstellen (indem Sie exhaustMapwie im obigen Code verwenden);
  • Fehler behandeln Anfragen;
  • Verhindern Sie, dass der Browser offline neue Anforderungen stellt.
Glukki
quelle