L'expression ___ a changé après avoir été vérifiée


288

Pourquoi le composant dans ce simple plunk

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message:string = 'loading :(';

  ngAfterViewInit() {
    this.updateMessage();
  }

  updateMessage(){
    this.message = 'all done loading :)'
  }
}

lancement:

EXCEPTION: l'expression «Je suis {{message}} dans l'application @ 0: 5» a changé après avoir été vérifiée. Valeur précédente: «Je charge :(». Valeur actuelle: «J'ai fini de charger :)» dans [Je suis {{message}} dans App @ 0: 5]

quand tout ce que je fais est de mettre à jour une liaison simple lorsque ma vue est lancée?



Pensez à modifier votre ChangeDetectionStrategylorsque vous utilisez detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas

Pensez simplement à un contrôle d'entrée et vous y remplissez des données dans une méthode et dans la même méthode, vous lui affectez une valeur. Le compilateur sera certainement confondu avec la valeur nouvelle / précédente. La liaison et le remplissage doivent donc se produire de différentes manières.
MAC

Réponses:


295

Comme indiqué par drewmoore, la solution appropriée dans ce cas consiste à déclencher manuellement la détection des modifications pour le composant actuel. Cela se fait à l'aide de la detectChanges()méthode de l' ChangeDetectorRefobjet (importé de angular2/core) ou de sa markForCheck()méthode, qui met également à jour les composants parents. Exemple pertinent :

import { Component, ChangeDetectorRef, AfterViewInit } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App implements AfterViewInit {
  message: string = 'loading :(';

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.message = 'all done loading :)'
    this.cdr.detectChanges();
  }

}

Voici également Plunkers démontrant la approches ngOnInit , setTimeout et enableProdMode au cas où.


7
Dans mon cas, j'ouvrais un modal. Après avoir ouvert le modal, il affichait le message "Expression ___ a changé après avoir été vérifié", donc ma solution a été ajoutée this.cdr.detectChanges (); après avoir ouvert mon modal. Merci!
Jorge Casariego

Où est votre déclaration pour la propriété cdr? Je m'attendrais à voir une ligne comme cdr : anyou quelque chose comme ça sous la messagedéclaration. Juste inquiet d'avoir raté quelque chose?
CodeCabbie

1
@CodeCabbie c'est dans les arguments du constructeur.
slasky

1
Cette résolution résout mon problème! Merci beaucoup! Manière très claire et simple.
lwozniak

3
Cela m'a aidé - avec quelques modifications. J'ai dû styliser les éléments li qui ont été générés via la boucle ngFor. Je devais changer la couleur des étendues dans les éléments de la liste en fonction de innerText en cliquant sur `` trier '' qui mettait à jour un booléen utilisé comme paramètre d'un tube de tri (le résultat trié était une copie des données, donc les styles n'obtenaient pas mis à jour avec ngStyle seul). - Au lieu d'utiliser 'AfterViewInit', j'ai utilisé 'AfterViewChecked' - J'ai également veillé à importer et à implémenter AfterViewChecked. note: régler le tuyau sur 'pure: false' ne fonctionnait pas, j'ai dû ajouter cette étape supplémentaire (:
Chloe Corrigan

171

Tout d'abord, notez que cette exception ne sera levée que lorsque vous exécutez votre application en mode dev (ce qui est le cas par défaut à partir de la beta-0): si vous appelez enableProdMode()lors du démarrage de l'application, elle ne sera pas levée ( voir plunk mis à jour ).

Deuxièmement, ne faites pas cela car cette exception est levée pour une bonne raison: en bref, en mode dev, chaque cycle de détection de changement est suivi immédiatement par un deuxième cycle qui vérifie qu'aucune liaison n'a changé depuis la fin du premier, car cela indiquerait que les changements sont causés par la détection des changements elle-même.

Dans votre plunk, la liaison {{message}}est modifiée par votre appel à setMessage(), qui se produit dans le ngAfterViewInitcrochet, qui se produit dans le cadre du tour de détection de changement initial. Cela en soi n'est pas problématique cependant - le problème est que setMessage()change la liaison mais ne déclenche pas une nouvelle ronde de détection de changement, ce qui signifie que cette modification ne sera détectée que lorsque une prochaine ronde de détection de changement sera déclenchée ailleurs.

Le point à retenir: tout ce qui modifie une liaison doit déclencher une ronde de détection des modifications lorsqu'il le fait.

Mise à jour en réponse à toutes les demandes d'un exemple de la façon de procéder : @ La solution de Tycho fonctionne, comme le font les trois méthodes de la réponse @MarkRajcok. Mais franchement, ils se sentent tous laids et mauvais pour moi, comme le genre de hacks sur lesquels nous nous sommes habitués en ng1.

Certes, il y a parfois des circonstances où ces hacks sont appropriées, mais si vous les utilisez sur quelque chose de plus qu'une très occasionnelle, il est un signe que vous vous battez le cadre plutôt que d' embrasser pleinement sa nature réactive.

À mon humble avis, une "façon angulaire2" plus idiomatique d'approcher cela est quelque chose comme: ( plunk )

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message | async}} </div>`
})
export class App {
  message:Subject<string> = new BehaviorSubject('loading :(');

  ngAfterViewInit() {
    this.message.next('all done loading :)')
  }
}

13
Pourquoi setMessage () ne déclenche-t-il pas un nouveau cycle de détection des modifications? Je pensais qu'Angular 2 déclenchait automatiquement la détection des modifications lorsque vous modifiez la valeur de quelque chose dans l'interface utilisateur.
Danny Libin

4
@drewmoore "Tout ce qui change une liaison doit déclencher une ronde de détection de changement quand il le fait". Comment? Est-ce une bonne pratique? Ne devrait-on pas tout terminer en une seule fois?
Daniel Birowsky Popeski

2
@Tycho, en effet. Depuis que j'ai écrit ce commentaire, j'ai répondu à une autre question où je décris les 3 façons d'exécuter la détection de changement , ce qui inclut detectChanges().
Mark Rajcok

4
juste pour noter que, dans le corps de la question actuelle, la méthode invoquée est nommée updateMessage, passetMessage
superjos

3
@Daynil, j'ai eu le même sentiment, jusqu'à ce que je lise le blog donné en commentaire sous la question: blog.angularindepth.com/… Il explique pourquoi cela doit être fait manuellement. Dans ce cas, la détection de changement angulaire a un cycle de vie. Dans le cas où quelque chose change une valeur entre ces cycles de vie, alors une détection de changement forcée doit être exécutée (ou une temporisation - qui s'exécute dans la boucle d'événement suivante, déclenchant à nouveau la détection de changement).
Mahesh

51

ngAfterViewChecked() travaillé pour moi:

import { Component, ChangeDetectorRef } from '@angular/core'; //import ChangeDetectorRef

constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
   //your code to update the model
   this.cdr.detectChanges();
}

Comme l'a mentionné Drew, le cycle de détection des changements angulaires, qui est biphasé, doit détecter les changements après la modification de la vue de l'enfant, et je pense que la meilleure façon de le faire est d'utiliser le crochet de cycle de vie fourni par angular lui-même et de demander à angular de manuellement détecter le changement et le lier. Mon opinion personnelle est que cela semble une réponse appropriée.
Joey587

1
Cela fonctionne pour moi, pour charger le composant dynamique de la hiérarchie à l'intérieur ensemble.
Mehdi Daustany

47

J'ai corrigé cela en ajoutant ChangeDetectionStrategy à partir du noyau angulaire.

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})

Cela a fonctionné pour moi. Je me demande quelle est la différence entre cela et l'utilisation de ChangeDetectorRef
Ari

1
Hmm ... Ensuite, le mode du détecteur de changement sera initialement réglé sur CheckOnce( documentation )
Alex Klaus

Oui, l'erreur / l'avertissement a disparu. Mais cela prend beaucoup plus de temps de chargement qu'avant, comme une différence de 5 à 7 secondes, ce qui est énorme.
Kapil Raghuwanshi

1
@KapilRaghuwanshi Exécutez this.cdr.detectChanges();après tout ce que vous essayez de charger. Parce que c'est peut-être parce que la détection de changement ne se déclenche pas
Harshal Carpenter

36

Vous ne pouvez pas utiliser ngOnInitcar vous venez de changer la variable membremessage ?

Si vous souhaitez accéder à une référence à un composant enfant @ViewChild(ChildComponent), vous devez en effet l'attendre avecngAfterViewInit .

Un correctif sale est d'appeler le updateMessage()dans la boucle d'événement suivante avec par exemple setTimeout.

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}

4
Changer mon code dans la méthode ngOnInit a fonctionné pour moi.
Faliorn

29

Pour cela, j'ai essayé les réponses ci-dessus, beaucoup ne fonctionnent pas dans la dernière version d'Angular (6 ou ultérieure)

J'utilise le contrôle du matériel qui a nécessité des modifications après la première liaison effectuée.

    export class AbcClass implements OnInit, AfterContentChecked{
        constructor(private ref: ChangeDetectorRef) {}
        ngOnInit(){
            // your tasks
        }
        ngAfterContentChecked() {
            this.ref.detectChanges();
        }
    }

Ajouter ma réponse ainsi, cela aide certains à résoudre un problème spécifique.


Cela a réellement fonctionné pour mon cas, mais vous avez une faute de frappe, après avoir implémenté AfterContentChecked, vous devez appeler ngAfterContentChecked, pas ngAfterViewInit.
Tomas Lukac

Je suis actuellement sur la version 8.2.0 :)
Tomas Lukac

1
@ TomášLukáč Merci pour la réponse, j'ai mis à jour ma réponse (y)
MarmiK

1
Je recommande cette réponse si aucune des réponses ci-dessus ne fonctionne pas.
Amir Choubani

afterContentChecked est appelé chaque fois que le DOM est recalculé ou revérifié. Intimating from Angular version 9
Imran Faruqi

24

L'article Tout ce que vous devez savoir sur l' ExpressionChangedAfterItHasBeenCheckedErrorerreur explique le comportement en détail.

Le problème avec votre configuration est que le ngAfterViewInithook de cycle de vie est exécuté après la détection des modifications mises à jour DOM traitées. Et vous modifiez effectivement la propriété utilisée dans le modèle dans ce hook, ce qui signifie que DOM doit être restitué:

  ngAfterViewInit() {
    this.message = 'all done loading :)'; // needs to be rendered the DOM
  }

et cela nécessitera un autre cycle de détection de changement et Angular de par sa conception n'exécute qu'un seul cycle de résumé.

Vous avez essentiellement deux alternatives pour le résoudre:

  • mettre à jour la propriété de manière asynchrone soit en utilisant setTimeout, Promise.thenou asynchrone observable dans le modèle référencé

  • effectuer la mise à jour de propriété dans un hook avant la mise à jour DOM - ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked.


Lisez vos articles: blog.angularindepth.com/… , en lira bientôt un autre blog.angularindepth.com/… . Impossible de trouver une solution à ce problème. pouvez-vous me dire ce qui se passe si j'ajoute le crochet de cycle de vie ngDoCheck ou ngAfterContentChecked et que j'ajoute ceci this.cdr.markForCheck (); (cdr pour ChangeDetectorRef) à l'intérieur. N'est-ce pas une bonne façon de vérifier les changements après le crochet du cycle de vie et la vérification ultérieure terminée.
Harsimer

9

Il vous suffit de mettre à jour votre message dans le bon crochet de cycle de vie, dans ce cas, c'est ngAfterContentCheckedau lieu de ngAfterViewInit, car dans ngAfterViewInit, une vérification du message variable a été démarrée mais n'est pas encore terminée.

voir: https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#afterview

donc le code sera juste:

import { Component } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message: string = 'loading :(';

  ngAfterContentChecked() {
     this.message = 'all done loading :)'
  }      
}

voir la démo de travail sur Plunker.


J'utilisais une combinaison d'un @ViewChildren()compteur basé sur la longueur qui était rempli par un observable. C'était la seule solution qui a fonctionné pour moi!
msanford

2
La combinaison de ngAfterContentCheckedet ChangeDetectorRefd'en haut a fonctionné pour moi. Sur ngAfterContentCheckedappelé -this.cdr.detectChanges();
Kunal Dethe

7

Cette erreur survient car la valeur existante est mise à jour immédiatement après avoir été initialisée. Donc, si vous mettez à jour une nouvelle valeur après le rendu de la valeur existante dans DOM, cela fonctionnera correctement.Comme mentionné dans cet article Débogage angulaire "L'expression a changé après avoir été vérifiée"

par exemple, vous pouvez utiliser

ngOnInit() {
    setTimeout(() => {
      //code for your new value.
    });

}

ou

ngAfterViewInit() {
  this.paginator.page
      .pipe(
          startWith(null),
          delay(0),
          tap(() => this.dataSource.loadLessons(...))
      ).subscribe();
}

Comme vous pouvez le voir, je n'ai pas mentionné le temps dans la méthode setTimeout. Comme il s'agit d'une API fournie par le navigateur, et non d'une API JavaScript, cela s'exécutera séparément dans la pile du navigateur et attendra la fin des éléments de la pile d'appels.

La façon dont l'API du navigateur suscite le concept est expliquée par Philip Roberts dans une vidéo Youtube (Qu'est-ce que le hack est une boucle d'événement?).


4

Vous pouvez également placer votre appel à updateMessage () dans la méthode ngOnInt () -, au moins cela fonctionne pour moi

ngOnInit() {
    this.updateMessage();
}

Dans RC1, cela ne déclenche pas l'exception


3

Vous pouvez également créer un minuteur à l'aide de la Observable.timerfonction rxjs , puis mettre à jour le message dans votre abonnement:                    

Observable.timer(1).subscribe(()=> this.updateMessage());

2

Il génère une erreur car votre code est mis à jour lorsque ngAfterViewInit () est appelé. Signifie que votre valeur initiale a été modifiée lorsque ngAfterViewInit a lieu, si vous appelez cela dans ngAfterContentInit (), cela ne générera pas d'erreur.

ngAfterContentInit() {
    this.updateMessage();
}

0

Je n'ai pas pu commenter le message de @Biranchi car je n'ai pas assez de réputation, mais cela a résolu le problème pour moi.

Une chose à noter! Si vous ajoutez changeDetection: ChangeDetectionStrategy.OnPush sur le composant n'a pas fonctionné, et son composant enfant (composant muet), essayez de l'ajouter également au parent.

Cela a corrigé le bug, mais je me demande quels sont les effets secondaires de cela.


0

J'ai eu une erreur similaire en travaillant avec datatable. Ce qui se passe, c'est lorsque vous utilisez * ngFor dans une autre table de données * ngFor, jetez cette erreur car elle intercepte le cycle de changement angulaire. SO au lieu d'utiliser datatable dans datatable, utilisez une table régulière ou remplacez mf.data par le nom du tableau. Cela fonctionne bien.


0

Je pense qu'une solution des plus simples serait la suivante:

  1. Faire une implémentation de l'attribution d'une valeur à une variable, c'est-à-dire via une fonction ou un setter.
  2. Créez une variable de classe (static working: boolean)dans la classe où cette fonction existe et chaque fois que vous appelez la fonction, rendez-la simplement vraie comme vous le souhaitez. Dans la fonction, si la valeur de travail est vraie, revenez tout de suite sans rien faire. sinon, effectuez la tâche que vous souhaitez. Assurez-vous de changer cette variable sur false une fois la tâche terminée, c'est-à-dire à la fin de la ligne de codes ou dans la méthode d'abonnement lorsque vous avez terminé d'affecter des valeurs!
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.