Angolare 2: vista non aggiornata dopo le modifiche al modello


128

Ho un semplice componente che chiama un API REST ogni pochi secondi e riceve alcuni dati JSON. Vedo dalle mie dichiarazioni di registro e dal traffico di rete che i dati JSON restituiti stanno cambiando e il mio modello è in fase di aggiornamento, tuttavia la vista non sta cambiando.

Il mio componente è simile a:

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

E la mia vista è simile a:

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

Posso vedere dai risultati console.log(this.recentDetections[0].macAddress)che l'oggetto recentDetections è in fase di aggiornamento, ma la tabella nella vista non cambia mai a meno che non ricarichi la pagina.

Faccio fatica a vedere cosa sto facendo di sbagliato qui. Qualcuno può aiutare?


Vi raccomando di codice di rendere più pulito e meno complessa: stackoverflow.com/a/48873980/571030
glukki

Risposte:


215

È possibile che il codice nel tuo servizio sia in qualche modo fuori dalla zona di Angular. Ciò interrompe il rilevamento delle modifiche. Questo dovrebbe funzionare:

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

Per altri modi per invocare il rilevamento delle modifiche, vedere Attivazione manuale del rilevamento delle modifiche in Angolare

Sono modi alternativi per invocare il rilevamento delle modifiche

ChangeDetectorRef.detectChanges()

per eseguire immediatamente il rilevamento delle modifiche per il componente corrente e i relativi figli

ChangeDetectorRef.markForCheck()

per includere il componente corrente la prossima volta che il rilevamento delle modifiche delle corse angolari

ApplicationRef.tick()

per eseguire il rilevamento delle modifiche per l'intera applicazione


9
Un'altra alternativa è iniettare ChangeDetectorRef e chiamare cdRef.detectChanges()invece di zone.run(). Questo potrebbe essere più efficiente, poiché non eseguirà il rilevamento delle modifiche sull'intero albero dei componenti come zone.run()fa.
Mark Rajcok,

1
Essere d'accordo. Utilizzare solo detectChanges()se le modifiche apportate sono locali per il componente e i suoi discendenti. La mia comprensione è che detectChanges()controllerà il componente e dei suoi discendenti, ma zone.run()e ApplicationRef.tick()controllerà l'intero albero componente.
Mark Rajcok,

2
esattamente quello che stavo cercando .. usare @Input()non aveva senso né funzionava.
yorch

15
Non capisco davvero perché abbiamo bisogno di tutta questa roba manuale o perché gli dei angular2 lo abbiano lasciato così necessario. Perché @Input non significa che il componente sia realmente dipendente da qualunque cosa il componente genitore abbia detto che dipendesse dai suoi dati variabili? Sembra estremamente inelegante che non funzioni. Cosa mi sto perdendo?

13
Ha funzionato per me. Ma questo è un altro esempio del perché ho la sensazione che gli sviluppatori della struttura angolare si perdano nella complessità della struttura angolare. Quando usi angular come sviluppatore di applicazioni devi dedicare così tanto tempo a capire quanto angular fa questo o quello e come ovviare a cose come le domande precedenti. Suona orribile !!
Karl,

32

In origine è una risposta nei commenti di @Mark Rajcok, ma voglio metterlo qui come testato e funzionato come soluzione usando ChangeDetectorRef , vedo un buon punto qui:

Un'altra alternativa è quella di iniettare ChangeDetectorRefe chiamare cdRef.detectChanges()invece di zone.run(). Questo potrebbe essere più efficiente, poiché non eseguirà il rilevamento delle modifiche sull'intero albero dei componenti come zone.run()fa. - Mark Rajcok

Quindi il codice deve essere come:

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

Modifica : l'utilizzo di .detectChanges()subscibe potrebbe causare un tentativo di utilizzare una vista distrutta: detectChanges

Per risolverlo è necessario unsubscribeprima di distruggere il componente, quindi il codice completo sarà come:

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

}

this.cdRef.detectChanges (); risolto il mio problema. Grazie!
Mangesh,

Grazie per il tuo suggerimento ma dopo più tentativi di chiamata ricevo un errore: errore: errore: ViewDestroyedError: tentativo di utilizzare una vista distrutta: detectChanges per me zone.run () mi aiuta a uscire da questo errore.
shivam srivastava,

8

Prova ad usare @Input() recentDetections: Array<RecentDetection>;

EDIT: il motivo per cui@Input()è importante è perché vuoi associare il valore nel file dattiloscritto / javascript alla vista (html). La vista si aggiornerà automaticamente se viene modificato un valore dichiarato con il@Input()decoratore. Se una@Input()o@Output()decoratore viene cambiato, unangOnChanges-Event attiverà, e la vista si aggiornerà con il nuovo valore. Si può dire che la@Input()volontà a due vie vincoli il valore.

cerca Input su questo link per angolare: glossario per maggiori informazioni

EDIT: dopo aver appreso di più sullo sviluppo di Angular 2, ho pensato che fare@Input()davvero non fosse la soluzione, e come menzionato nei commenti,

@Input() si applica solo quando i dati vengono modificati mediante associazione dei dati dall'esterno del componente (i dati associati nel padre sono cambiati) e non quando i dati vengono modificati dal codice all'interno del componente.

Se dai un'occhiata alla risposta di Günter, è una soluzione più accurata e corretta al problema. Terrò ancora questa risposta qui, ma per favore segui la risposta di Günter come quella corretta.


2
Questo è stato. Avevo già visto l'annotazione @Input () ma l'ho sempre associata agli input del modulo, non avrei mai pensato che avrei avuto bisogno qui. Saluti.
Matt Watson,

1
@Giovanni grazie per la spiegazione aggiuntiva. Ma ciò che hai aggiunto si applica solo quando i dati vengono modificati mediante associazione dei dati dall'esterno del componente (i dati associati nel padre sono cambiati) e non quando i dati vengono modificati dal codice all'interno del componente.
Günter Zöchbauer,

C'è un'associazione di proprietà allegata quando si utilizza la @Input()parola chiave e tale associazione assicura che il valore sia aggiornato nella vista. il valore farà parte di un evento del ciclo di vita. Questo post contiene alcune ulteriori informazioni sulla @Input()parola chiave
John,

Mi arrendo. Non vedo dove @Input()è coinvolto un qui o perché dovrebbe essere e la domanda non fornisce abbastanza informazioni. Grazie comunque per la tua pazienza :)
Günter Zöchbauer,

3
@Input()è più una soluzione alternativa che nasconde la radice del problema. I problemi devono essere correlati alle zone e al rilevamento delle modifiche.
magiccrafter,

2

Nel mio caso, ho avuto un problema molto simile. Stavo aggiornando la mia vista all'interno di una funzione chiamata da un componente padre e nel mio componente padre ho dimenticato di usare @ViewChild (NameOfMyChieldComponent). Ho perso almeno 3 ore solo per questo stupido errore. cioè: non avevo bisogno di usare nessuno di questi metodi:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()

1
puoi spiegare come hai aggiornato il componente figlio con @ViewChild? grazie
Lucas Colombo,

Sospetto che un genitore chiamasse un metodo di una classe figlio che non era stato reso ed esisteva solo nella memoria
glukki

1

Invece di gestire le zone e rilevare le modifiche, lascia che AsyncPipe gestisca la complessità. Ciò comporterà l'abbonamento osservabile, l'annullamento dell'iscrizione (per evitare perdite di memoria) e il rilevamento delle modifiche sulle spalle angolari.

Cambia la tua classe per renderla osservabile, che emetterà risultati di nuove richieste:

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

E aggiorna la visualizzazione per utilizzare AsyncPipe:

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

Vuoi aggiungere, che è meglio creare un servizio con un metodo che prenderà intervalargomento e:

  • creare nuove richieste (usando exhaustMapcome nel codice sopra);
  • gestire gli errori delle richieste;
  • impedisce al browser di effettuare nuove richieste offline.
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.