42 votes

Angular2 - L'expression a changé après avoir été cochée - Liaison à div width avec des événements de redimensionnement

J'ai fait un peu de la lecture et de l'enquête sur cette erreur, mais vous ne savez pas ce que la bonne réponse est dans ma situation. Je comprends que le dev mode, de la détection de changement se déroule deux fois, mais je suis réticent à l'utiliser enableProdMode() pour masquer le problème.

Voici un exemple simple où le nombre de cellules dans le tableau devrait augmenter à mesure que la largeur de la div et de l'expansion. (Notez que la largeur de la div n'est pas une fonction seulement de la largeur de l'écran, donc @Media ne peut pas facilement être appliquée)

Mon code HTML se présente comme suit (widget.template.html):

<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
   <td>Value1</td>
   <td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
   <td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>

Ce sur son propre ne fait rien. Je suppose que c'est parce que rien n'est à l'origine de la détection de changement de se produire. Cependant, quand j'ai changer la première ligne à la suivante, et de créer une fonction vide pour recevoir l'appel, il commence à travailler, mais parfois je reçois l'Expression a changé après qu'il a été vérifié erreur"

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

Ma meilleure supposition est que, avec cette modification, le changement de détection est déclenchée et tout commence à réagir, cependant, lorsque la largeur des changements rapidement l'exception est levée, car l'itération précédente de détection de changement a pris plus de temps que de changer la largeur de la div.

  1. Est-il une meilleure approche pour le déclenchement de la détection de changement?
  2. Je devrais être en train de capturer l'événement resize grâce à une fonction afin de s'assurer la détection de changement se produit?
  3. Est l'aide de #widthParentDiv pour accéder à la la largeur de la div acceptable?
  4. Est-il une meilleure solution?

Pour plus de détails sur mon projet, veuillez voir cette question semblable.

Merci

68voto

Mark Rajcok Points 85912

Pour résoudre votre problème, vous simplement besoin d'obtenir et stocker la taille de l' div dans une propriété d'un composant après chaque événement de redimensionnement, et d'utiliser cette propriété dans le modèle. De cette façon, la valeur va rester constante lors de la 2e manche de la détection de changement s'exécute en dev mode.

Je recommande également à l'aide de @HostListener plutôt que d'ajouter (window:resize) de votre modèle. Nous allons utiliser @ViewChild pour obtenir une référence à l' div. Et nous allons utiliser le cycle de vie de crochet ngAfterViewInit() pour définir la valeur initiale.

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

@Component({
  selector: 'my-app',
  template: `<div #widgetParentDiv class="Content">
    <p>Sample widget</p>
    <table><tr>
       <td>Value1</td>
       <td *ngIf="divWidth > 350">Value2</td>
       <td *ngIf="divWidth > 700">Value3</td>
      </tr>
    </table>`,
})
export class AppComponent {
  divWidth = 0;
  @ViewChild('widgetParentDiv') parentDiv:ElementRef;
  @HostListener('window:resize') onResize() {
    // guard against resize before view is rendered
    if(this.parentDiv) {
       this.divWidth = this.parentDiv.nativeElement.clientWidth;
    }
  }
  ngAfterViewInit() {
    this.divWidth = this.parentDiv.nativeElement.clientWidth;
  }
}

Dommage que cela ne fonctionne pas. Nous obtenons

L'Expression a changé après qu'il ait été vérifié. Valeur précédente: 'false'. Valeur actuelle: 'true'.

L'erreur est de se plaindre de notre - NgIf expressions: la première fois qu'il s'exécute, divWidth est égal à 0, ngAfterViewInit() pistes et les changements de la valeur à quelque chose d'autre que 0, alors le 2ème tour de la détection de changement de pistes (en dev mode). Heureusement, il est facile de/solution connue, et c'est une seule fois question, non pas d'un problème récurrent comme dans l'OP:

  ngAfterViewInit() {
    // wait a tick to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  }

Notez que cette technique, de l'attente d'une tique est documenté ici: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-de-vue-enfant

Souvent, en ngAfterViewInit() et ngAfterViewChecked() , nous allons avoir besoin d'employer l' setTimeout() tour parce que ces méthodes sont appelées après que le composant est d'avis est composé.

Voici un travail plunker.


Nous pouvons faire mieux. Je pense que nous devrions augmenter la vitesse de la redimensionner des événements tels que Angulaires de la détection de changement ne s'exécute que, disons, 100-250ms, plutôt que chaque fois qu'un événement de redimensionnement se produit. Cela devrait permettre d'éviter l'application d'arriver là où l'utilisateur est le redimensionnement de la fenêtre, parce qu'en ce moment, chaque événement de redimensionnement des causes de la détection de changement à exécuter (deux fois en dev mode). Vous pouvez vérifier cela, ajoutez la méthode suivante à la précédente plunker:

ngDoCheck() {
   console.log('change detection');
}

Observables peuvent facilement gaz d'événements, donc au lieu d'utiliser @HostListener de lier l'événement resize, nous allons créer un observables:

Observable.fromEvent(window, 'resize')
   .throttleTime(200)
   .subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );

Cela fonctionne, mais... tout en expérimentant avec qui, j'ai découvert quelque chose de très intéressant... même si nous étouffer l'événement de redimensionnement, angle de détection de changement de toujours s'exécute à chaque fois il y a un événement de redimensionnement. I. e., la limitation n'a pas d'incidence sur la fréquence de la détection de changement s'exécute. (Tobias Bosch confirmé ce: https://github.com/angular/angular/issues/1773#issuecomment-102078250.)

Je veux seulement la détection de changement à exécuter si l'événement passe la manette des gaz du temps. Et je n'ai besoin que de la détection de changement de courir sur cette composante. La solution est de créer les observables de l'extérieur de la zone Angulaire, puis manuellement appel à la détection de changement à l'intérieur de l'abonnement rappel:

constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
  // set initial value, but wait a tick to avoid one-time devMode
  // unidirectional-data-flow-violation error
  setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  this.ngzone.runOutsideAngular( () =>
     Observable.fromEvent(window, 'resize')
       .throttleTime(200)
       .subscribe(_ => {
          this.divWidth = this.parentDiv.nativeElement.clientWidth;
          this.cdref.detectChanges();
       })
  );
}

Voici un travail plunker.

Dans le plunker j'ai ajouté un counter que je incrémenter chaque détection de changement de cycle, cycle de vie du crochet ngDoCheck(). Vous pouvez voir que cette méthode n'est pas appelée – la valeur du compteur ne change pas sur les événements de redimensionnement.

detectChanges() sera exécuté à la détection de changement sur ce composant et de ses enfants. Si vous préférez exécuter la détection de changement à partir de la racine du composant (c'est à dire, de fonctionner à plein détection de changement de contrôle), puis utiliser ApplicationRef.tick() à la place (ce est commenté dans la plunker). Notez que tick() entraînera ngDoCheck() d'être appelé.


C'est une grande question. J'ai passé beaucoup de temps à essayer différentes solutions et j'ai beaucoup appris. Merci de poster cette question.

17voto

Luiz C. Junior Points 129

Autre moyen que j'ai utilisé pour résoudre ceci:

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


@Component({
  selector: 'your-seelctor',
  template: 'your-template',
})

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X