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

129

J'ai un composant simple qui appelle une API REST toutes les quelques secondes et reçoit en retour des données JSON. Je peux voir à partir de mes instructions de journal et du trafic réseau que les données JSON renvoyées changent et que mon modèle est en cours de mise à jour, mais 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 mon point de 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>

Je peux voir d'après les résultats console.log(this.recentDetections[0].macAddress)que 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 de mal ici. Quelqu'un peut-il aider?

Matt Watson
la source
Je recommande de rendre le code plus propre et moins complexe: stackoverflow.com/a/48873980/571030
glukki

Réponses:

216

Il se peut que le code de votre service sorte d'une manière ou d'une autre de la zone d'Angular. Cela rompt 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'appeler la détection des modifications, voir Déclenchement manuel de la détection de changement dans Angular

D'autres moyens d'invoquer la détection des modifications sont

ChangeDetectorRef.detectChanges()

pour exécuter immédiatement la détection des modifications pour le composant actuel et ses enfants

ChangeDetectorRef.markForCheck()

pour inclure le composant actuel la prochaine fois qu'Angular exécute la détection de changement

ApplicationRef.tick()

pour exécuter la détection des modifications pour l'ensemble de l'application

Günter Zöchbauer
la source
9
Une autre alternative consiste à injecter ChangeDetectorRef et à appeler à la cdRef.detectChanges()place de zone.run(). Cela pourrait être plus efficace, car il zone.run()n'exécutera pas de détection de changement sur l'ensemble de l'arborescence des composants comme le fait.
Mark Rajcok
1
Se mettre d'accord. À n'utiliser que detectChanges()si les modifications que vous apportez sont locales pour le composant et ses descendants. Je crois comprendre que detectChanges()cela vérifiera le composant et ses descendants, mais zone.run()et ApplicationRef.tick()vérifiera l'ensemble de l'arborescence des composants.
Mark Rajcok
2
exactement ce que je cherchais .. l'utilisation @Input()n'avait pas de sens ni fonctionnait.
yorch
15
Je ne comprends vraiment pas pourquoi nous avons besoin de tout ce manuel ou pourquoi angular2 gods est devenu si nécessaire. Pourquoi @Input ne signifie-t-il pas que le composant dépend vraiment de tout ce que le composant parent a dit qui dépendait de ses données changeantes? Il semble extrêmement inélégant que cela ne fonctionne pas seulement. Qu'est-ce que je rate?
13
A travaillé pour moi. Mais ceci est un autre exemple pour lequel j'ai le sentiment que les développeurs de framework angulaire sont perdus dans la complexité du framework angulaire. Lorsque vous utilisez angular en tant que développeur d'applications, vous devez passer beaucoup de temps à comprendre comment angular fait ceci ou fait cela et comment contourner les choses comme celles posées ci-dessus. Terrible !!
Karl
32

C'est à l'origine une réponse dans les commentaires de @Mark Rajcok, mais je veux le placer ici comme une solution testée et travaillée en utilisant ChangeDetectorRef , je vois un bon point ici:

Une autre alternative est d'injecter ChangeDetectorRefet d'appeler cdRef.detectChanges()au lieu de zone.run(). Cela pourrait être plus efficace, car il zone.run()n'exécutera pas de détection de changement sur l'ensemble de l'arborescence des composants comme le fait. - Mark Rajcok

Le code doit donc ê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()subscibe à l'intérieur peut entraîner un problème.Tentative d'utilisation d'une vue détruite: detectChanges

Pour le résoudre, vous devez le faire unsubscribeavant de détruire le composant, le code complet ressemblera donc à:

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
la source
this.cdRef.detectChanges (); résolu mon problème. Merci!
mangesh
Merci pour votre suggestion, mais après plusieurs tentatives d'appel, j'obtiens une erreur: échec: Erreur: ViewDestroyedError: Tentative d'utilisation d'une vue détruite: detectChanges Pour moi, zone.run () m'aide à sortir de cette erreur.
shivam srivastava le
8

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

EDIT: La raison pour laquelle@Input() est importante est que vous souhaitez lier la valeur du fichier dactylographié / javascript à la vue (html). La vue se mettra à jour si une valeur déclarée avec le @Input()décorateur est modifiée. Si un décorateur @Input()ou @Output()est modifié, un ngOnChanges-event se déclenchera et la vue se mettra à jour avec la nouvelle valeur. Vous pouvez dire que le@Input() va lier la valeur dans les deux sens.

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

ÉDITER: après en avoir appris plus sur le développement d'Angular 2, j'ai pensé que faire un @Input()n'est 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 depuis l'extérieur du composant (les données liées dans le parent ont été modifiées) et non lorsque 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 plus correcte au problème. Je garderai toujours cette réponse ici, mais veuillez suivre la réponse de Günter comme étant la bonne.

John
la source
2
C'était ça. J'avais déjà vu l'annotation @Input () mais je l'avais toujours associée aux entrées de formulaire, je n'avais jamais pensé que j'en aurais besoin ici. À votre santé.
Matt Watson
1
@John merci pour l'explication supplémentaire. Mais ce que vous avez ajouté ne s'applique que lorsque les données sont modifiées par liaison de données depuis l'extérieur du composant (les données liées dans le parent ont été modifiées) et non lorsque les données sont modifiées à partir du code dans le composant.
Günter Zöchbauer
Une liaison de propriété est attachée lors de l'utilisation du @Input()mot clé, et cette liaison garantit que la valeur est mise à jour dans la vue. la valeur fera partie d'un événement du cycle de vie. Ce message contient plus d'informations sur le @Input()mot-clé
Jean
J'abandonnerai. Je ne vois pas où un @Input()est impliqué ici ou pourquoi il devrait l'être et la question ne fournit pas suffisamment d'informations. Merci pour votre patience quand même :)
Günter Zöchbauer
3
@Input()est plus une solution de contournement qui cache la racine du problème. Les problèmes doivent être liés aux zones et à la détection des changements.
magiccrafter
2

Dans mon cas, j'ai eu un problème très similaire. J'étais en train de mettre à jour ma vue dans une fonction qui était appelée par un composant parent, et dans mon composant parent, j'ai oublié d'utiliser @ViewChild (NameOfMyChieldComponent). J'ai perdu au moins 3 heures rien que pour cette stupide erreur. ie: je n'ai pas eu besoin d'utiliser l' une de ces méthodes:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
user3674783
la source
1
pouvez-vous expliquer comment vous avez mis à jour votre composant enfant avec @ViewChild? merci
Lucas Colombo
Je soupçonne que le parent appelait une méthode d'une classe enfant qui n'a pas été rendue et qui n'existait qu'en mémoire
glukki
1

Au lieu de s'occuper des zones et de la détection des changements, laissez AsyncPipe gérer la complexité. Cela mettra l'abonnement observable, le désabonnement (pour éviter les fuites de mémoire) et la détection des changements sur les épaules angulaires.

Changez votre classe pour créer un observable, qui émettra les résultats des nouvelles requêtes:

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>

Vous voulez ajouter, qu'il est préférable de créer un service avec une méthode qui prendra intervalargument, et:

  • créer de nouvelles demandes (en utilisant exhaustMapcomme dans le code ci-dessus);
  • gérer les erreurs de demandes;
  • empêcher le navigateur de faire de nouvelles demandes en mode hors connexion.
glukki
la source