web-dev-qa-db-fra.com

Angular 2 - La vue ne se met pas à jour après les modifications du modèle

J'ai un composant simple qui appelle une REST api toutes les quelques secondes et qui reçoit des données JSON. D'après mes instructions de journal et le trafic réseau, les données JSON renvoyées changent et mon modèle est en cours de mise à jour. Toutefois, la vue ne change pas.

Mon composant ressemble à:

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

Et ma vue ressemble à:

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

D'après les résultats de console.log(this.recentDetections[0].macAddress), l'objet recentDetections est en cours de mise à jour, mais le tableau de la vue ne change jamais, à moins que je ne recharge la page.

J'ai du mal à voir ce que je fais mal ici. Quelqu'un peut-il aider?

98
Matt Watson

Il se peut que le code de votre service sorte de la zone angulaire. Cela interrompt la détection des changements. Cela devrait fonctionner:

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

Pour d'autres moyens d'invoquer la détection de changement, voir Déclencher la détection de changement manuellement dans Angular

Il existe d'autres moyens d'invoquer la détection de changement. 

ChangeDetectorRef.detectChanges()

pour exécuter immédiatement la détection de changement pour le composant actuel et ses enfants

ChangeDetectorRef.markForCheck()

d'inclure le composant actuel la prochaine fois que la détection de changement angulaire est exécutée

ApplicationRef.tick()

exécuter la détection de changement pour toute l'application

166
Günter Zöchbauer

C'est à l'origine une réponse dans les commentaires de @Mark Rajcok, mais je souhaite le placer ici en tant que solution testée et fonctionnant à l'aide de ChangeDetectorRef , je vois un bon point ici:

Une autre solution consiste à injecter ChangeDetectorRef et à appeler cdRef.detectChanges() au lieu de zone.run(). Cela pourrait être plus efficace, car il n’exécutera pas de détection de changement sur l’ensemble arborescence des composants comme zone.run() fait. - Mark Rajcok

Donc, le code doit être comme:

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

Edit: L'utilisation de .detectChanges() dans subscibe pourrait poser problème Tentative d'utilisation d'une vue détruite: detectChanges

Pour le résoudre, vous devez unsubscribe avant de détruire le composant, ainsi le code complet ressemblera à ceci:

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

}
16
Al-Mothafar

Essayez d'utiliser @Input() recentDetections: Array<RecentDetection>;

EDIT: Si @Input() est important, c'est parce que vous souhaitez lier la valeur du fichier TypeScript/javascript à la vue (html). La vue se mettra à jour si une valeur déclarée avec le décorateur @Input() est modifiée. Si un décorateur @Input() ou @Output() est modifié, un événement ngOnChanges- se déclenchera et la vue se mettra à jour avec la nouvelle valeur. Vous pouvez dire que @Input() liera la valeur dans les deux sens.

rechercher Entrée sur ce lien par angulaire: glossaire pour plus d'informations

EDIT: après en savoir plus sur le développement de Angular 2, je me suis dit que faire un @Input() n'était vraiment pas la solution, et comme mentionné dans les commentaires,

@Input() s'applique uniquement lorsque les données sont modifiées par liaison de données à partir de en dehors du composant (les données liées dans le parent ont été modifiées) et non lorsque le les données sont modifiées à partir du code dans le composant.

Si vous regardez @ la réponse de Günter, c'est une solution plus précise et correcte au problème. Je garderai toujours cette réponse ici, mais veuillez suivre la réponse de Günter comme étant la bonne.

7
John

Dans mon cas, j'ai eu un problème très similaire. Je mettais à jour ma vue dans une fonction appelée par un composant parent et dans mon composant parent, j'avais oublié d'utiliser @ViewChild (NameOfMyChieldComponent). J'ai perdu au moins 3 heures juste pour cette erreur stupide. i.e: Je n'ai pas eu besoin d'utiliser aucune de ces méthodes: 

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

Au lieu de gérer les zones et de détecter les modifications, laissez AsyncPipe gérer la complexité. Cela mettra les abonnements observables, les désabonnements (pour éviter les fuites de mémoire) et la détection des modifications sur les épaules angulaires.

Changez votre classe pour faire un observable, qui émettra les résultats de nouvelles demandes:

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

Et mettez à jour votre vue pour utiliser AsyncPipe:

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

Voulez-vous ajouter qu'il est préférable de créer un service avec une méthode qui prendra l'argument interval et:

  • créer de nouvelles demandes (en utilisant exhaustMap comme dans le code ci-dessus);
  • gérer les erreurs de demande;
  • empêche le navigateur de faire de nouvelles demandes en mode hors connexion.
0
glukki