Kątowy 2 - widok nie jest aktualizowany po zmianach modelu

129

Mam prosty komponent, który co kilka sekund wywołuje REST API i odbiera pewne dane JSON. Widzę z moich instrukcji dziennika i ruchu sieciowego, że zwracane dane JSON zmieniają się, a mój model jest aktualizowany, jednak widok się nie zmienia.

Mój komponent wygląda tak:

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

A mój widok wygląda następująco:

<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>

Widzę z wyników tego, console.log(this.recentDetections[0].macAddress)że obiekt latestDetections jest aktualizowany, ale tabela w widoku nigdy się nie zmienia, chyba że ponownie załaduję stronę.

Staram się zobaczyć, co tu robię źle. Czy ktoś może pomóc?

Matt Watson
źródło
Polecam do kodu podejmować bardziej czyste i mniej skomplikowane: stackoverflow.com/a/48873980/571030
glukki

Odpowiedzi:

216

Może się zdarzyć, że kod w Twojej usłudze w jakiś sposób wyrwie się ze strefy Angulara. To przerywa wykrywanie zmian. To powinno działać:

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

Aby poznać inne sposoby wywoływania wykrywania zmian, zobacz Ręczne wyzwalanie wykrywania zmian w Angular

Alternatywne sposoby wywoływania wykrywania zmian to

ChangeDetectorRef.detectChanges()

aby natychmiast uruchomić wykrywanie zmian dla bieżącego składnika i jego elementów podrzędnych

ChangeDetectorRef.markForCheck()

aby włączyć bieżący komponent następnym razem, gdy Angular uruchomi wykrywanie zmian

ApplicationRef.tick()

aby uruchomić wykrywanie zmian dla całej aplikacji

Günter Zöchbauer
źródło
9
Inną alternatywą jest wstrzyknięcie ChangeDetectorRef i wywołanie cdRef.detectChanges()zamiast zone.run(). Może to być bardziej wydajne, ponieważ nie uruchomi wykrywania zmian w całym drzewie komponentów, tak jak zone.run()ma to miejsce w przypadku.
Mark Rajcok
1
Zgodzić się. Używaj tylko detectChanges()wtedy, gdy jakiekolwiek wprowadzone zmiany są lokalne dla składnika i jego elementów podrzędnych. Rozumiem, że detectChanges()sprawdzi komponent i jego potomki, ale zone.run()i ApplicationRef.tick()sprawdzi całe drzewo komponentów.
Mark Rajcok
2
dokładnie to, czego szukałem… używanie @Input()nie miało sensu ani nie działało.
yorch
15
Naprawdę nie rozumiem, dlaczego potrzebujemy tych wszystkich podręczników ani dlaczego angular2 bogowie pozostawili takie konieczne. Dlaczego @Input nie oznacza, że ​​ten komponent naprawdę jest zależny od tego, co powiedział komponent nadrzędny, co zależało od zmieniających się danych? Wydaje się niezwykle nieeleganckie, że to nie tylko działa. czego mi brakuje?
13
Pracował dla mnie. Ale to jest kolejny przykład, dlaczego mam wrażenie, że programiści frameworków kątowych są zagubieni w złożoności tej struktury. Używając Angulara jako programisty aplikacji, musisz poświęcić tyle czasu, aby zrozumieć, jak Angular to robi lub robi i jak obejść takie rzeczy, jak kwestionowane powyżej. Okropne !!
Karl
32

Pierwotnie jest to odpowiedź w komentarzach @Mark Rajcok, ale chcę ją tutaj umieścić jako przetestowane i działające jako rozwiązanie wykorzystujące ChangeDetectorRef , widzę tutaj dobry punkt:

Inną alternatywą jest wstrzyknięcie ChangeDetectorRefi wywołanie cdRef.detectChanges()zamiast zone.run(). Może to być bardziej wydajne, ponieważ nie uruchomi wykrywania zmian w całym drzewie komponentów, tak jak zone.run()ma to miejsce w przypadku. - Mark Rajcok

Więc kod musi wyglądać następująco:

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

Edycja : użycie .detectChanges()inside subscibe może spowodować problem. Próba użycia zniszczonego widoku: DetectChanges

Aby go rozwiązać, musisz unsubscribeprzed zniszczeniem komponentu, więc pełny kod będzie wyglądał następująco:

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
źródło
this.cdRef.detectChanges (); naprawił mój problem. Dziękuję Ci!
mangesh
Dziękuję za sugestię, ale po wielu próbach połączenia pojawia się błąd: niepowodzenie: Błąd: ViewDestroyedError: Próba użycia zniszczonego widoku: DetectChanges Dla mnie zone.run () pomaga mi wyjść z tego błędu.
shivam srivastava
8

Spróbuj użyć @Input() recentDetections: Array<RecentDetection>;

EDYCJA: Powodem, dla którego@Input()jest to ważne, jest to, że chcesz powiązać wartość w pliku maszynopisu / javascript z widokiem (html). Widok zostanie zaktualizowany, jeśli wartość zadeklarowana w@Input()dekoratorze zostanie zmieniona. Jeślidekorator@Input()lub@Output()zostanie zmieniony,ngOnChangeswyzwolone zostaniezdarzenie-event, a widok zaktualizuje się o nową wartość. Możesz powiedzieć, że@Input()wola wiąże wartość w obie strony.

wyszukaj dane wejściowe dla tego łącza według kątów: słownik, aby uzyskać więcej informacji

EDYCJA: po tym, jak dowiedziałem się więcej o rozwoju Angular 2, doszedłem do wniosku, że robienie@Input()naprawdę nie jest rozwiązaniem i jak wspomniano w komentarzach,

@Input() ma zastosowanie tylko wtedy, gdy dane są zmieniane przez wiązanie danych spoza komponentu (zmienione dane powiązane w elemencie nadrzędnym), a nie gdy dane są zmieniane z kodu wewnątrz komponentu.

Jeśli spojrzysz na odpowiedź @ Güntera, jest to dokładniejsze i poprawne rozwiązanie problemu. Nadal zachowam tę odpowiedź tutaj, ale proszę postępuj zgodnie z odpowiedzią Güntera jako poprawną.

Jan
źródło
2
To było to. Widziałem wcześniej adnotację @Input (), ale zawsze kojarzyłem ją z danymi wejściowymi formularza, nigdy nie myślałem, że będę go tutaj potrzebować. Twoje zdrowie.
Matt Watson
1
@John dzięki za dodatkowe wyjaśnienie. Ale to, co dodałeś, ma zastosowanie tylko wtedy, gdy dane zostaną zmienione przez powiązanie danych spoza komponentu (zmienione dane powiązane w elemencie nadrzędnym), a nie wtedy, gdy dane zostaną zmienione z kodu w składniku.
Günter Zöchbauer
Podczas używania @Input()słowa kluczowego istnieje powiązanie właściwości , które zapewnia, że ​​wartość jest aktualizowana w widoku. wartość będzie częścią zdarzenia w cyklu życia. Ten post zawiera więcej informacji na temat @Input()słowa kluczowego
Jana
Poddam się. Nie widzę, gdzie @Input()jest tu zaangażowany ani dlaczego powinien być, a pytanie nie dostarcza wystarczających informacji. W każdym razie dziękuję za cierpliwość :)
Günter Zöchbauer
3
@Input()to raczej obejście, które ukrywa źródło problemu. Zagadnienia muszą być związane ze strefami i wykrywaniem zmian.
magiccrafter
2

W moim przypadku miałem bardzo podobny problem. Aktualizowałem swój widok w funkcji, która była wywoływana przez komponent nadrzędny, aw komponencie nadrzędnym zapomniałem użyć @ViewChild (NameOfMyChieldComponent). Przez ten głupi błąd straciłem przynajmniej 3 godziny. tj .: nie musiałem używać żadnej z tych metod:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
user3674783
źródło
1
Czy możesz wyjaśnić, w jaki sposób zaktualizowałeś swój komponent podrzędny za pomocą @ViewChild? dzięki
Lucas Colombo
Podejrzewam, że rodzic dzwoni metodę klasy dziecka, które nie zostało wydane, i istniał tylko w pamięci
glukki
1

Zamiast zajmować się strefami i wykrywaniem zmian - pozwól AsyncPipe zająć się złożonością. Spowoduje to umieszczenie obserwowalnej subskrypcji, anulowania subskrypcji (aby zapobiec wyciekom pamięci) i wykrywanie zmian na barkach Angulara.

Zmień swoją klasę tak, aby była obserwowalna, która będzie emitować wyniki nowych żądań:

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

I zaktualizuj widok, aby używać AsyncPipe:

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

Chcemy dodać, że lepiej wykonać usługę metodą, która będzie wymagała intervalargumentacji i:

  • tworzyć nowe żądania (używając exhaustMapjak w kodzie powyżej);
  • obsługi błędów żądań;
  • zatrzymaj przeglądarkę przed wysyłaniem nowych żądań w trybie offline.
glukki
źródło