Angular2 – L’espressione è cambiata dopo che è stata selezionata: associazione alla larghezza div con gli eventi di ridimensionamento

Ho letto e investigato su questo errore, ma non sono sicuro di quale sia la risposta corretta per la mia situazione. Capisco che in modalità dev, il rilevamento delle modifiche viene eseguito due volte, ma sono riluttante a utilizzare enableProdMode() per mascherare il problema.

Ecco un semplice esempio in cui il numero di celle nella tabella dovrebbe aumentare man mano che la larghezza del div si espande. (Si noti che la larghezza del div non è una funzione della sola larghezza dello schermo, quindi @Media non può essere facilmente applicato)

Il mio HTML appare come segue (widget.template.html):

 

Sample widget

Value1 350">Value2 700">Value3

Questo da solo non fa nulla. Suppongo che questo non accada perché nulla sta causando il rilevamento dei cambiamenti. Tuttavia, quando cambio la prima riga di seguito e creo una funzione vuota per ricevere la chiamata, inizia a funzionare, ma occasionalmente viene visualizzato il messaggio ‘Espressione è cambiata dopo che è stato controllato l’errore’

 
gets replaced with

La mia ipotesi migliore è che con questa modifica, il rilevamento delle modifiche sia triggersto e tutto inizi a rispondere, tuttavia, quando la larghezza cambia rapidamente, l’eccezione viene generata perché la precedente iterazione del rilevamento delle modifiche impiega più tempo a completare della modifica della larghezza del div.

  1. C’è un approccio migliore per triggersre il rilevamento dei cambiamenti?
  2. Devo catturare l’evento di ridimensionamento tramite una funzione per garantire che si verifichi il rilevamento del cambiamento?
  3. Sta usando #widthParentDiv per accedere alla larghezza del div accettabile?
  4. C’è una soluzione globale migliore?

Per maggiori dettagli sul mio progetto, vedi questa domanda simile.

Grazie

Per risolvere il problema, è sufficiente acquisire e archiviare la dimensione del div in una proprietà del componente dopo ogni evento di ridimensionamento e utilizzare tale proprietà nel modello. In questo modo, il valore rimarrà costante quando il secondo round di rilevamento del cambiamento verrà eseguito in modalità dev.

Raccomando anche di usare @HostListener piuttosto che aggiungere (window:resize) al modello. Useremo @ViewChild per ottenere un riferimento al div . E useremo il ciclo di vita hook ngAfterViewInit() per impostare il valore iniziale.

 import {Component, ViewChild, HostListener} from '@angular/core'; @Component({ selector: 'my-app', template: `

Sample widget

Value1 Value2 Value3
`, }) 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; } }

Peccato che non funzioni. Noi abbiamo

L’espressione è cambiata dopo che è stata controllata. Valore precedente: ‘falso’. Valore attuale: ‘true’.

L’errore si lamenta delle nostre espressioni NgIf : la prima volta che viene eseguito, divWidth è 0, quindi ngAfterViewInit() viene eseguito e cambia il valore in qualcosa di diverso da 0, quindi viene eseguito il secondo round di rilevamento modifiche (in modalità dev). Per fortuna, c’è una soluzione facile / conosciuta, e questo è un problema una tantum, non un problema continuo come nell’OP:

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

Si noti che questa tecnica, di attesa di un tick è documentata qui: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child

Spesso, in ngAfterViewInit() e ngAfterViewChecked() dovremo utilizzare il trucco setTimeout() perché questi metodi vengono chiamati dopo che la vista del componente è stata composta.

Ecco un plunker funzionante.


Possiamo migliorare questo Penso che dovremmo limitare gli eventi di ridimensionamento in modo tale che il rilevamento del cambiamento angular venga eseguito, ad esempio, ogni 100-250 ms, piuttosto che ogni volta che si verifica un evento di ridimensionamento. Ciò dovrebbe impedire all’app di rallentare quando l’utente sta ridimensionando la finestra, perché in questo momento, ogni evento di ridimensionamento determina l’esecuzione del rilevamento delle modifiche (due volte in modalità dev). Puoi verificarlo aggiungendo il seguente metodo al plunker precedente:

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

Le osservabili possono facilmente @HostListener eventi, quindi, anziché utilizzare @HostListener per collegarsi all’evento di ridimensionamento, creeremo un osservabile:

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

Funziona, ma … durante la sperimentazione ho scoperto qualcosa di molto interessante … anche se limitiamo l’evento di ridimensionamento, il rilevamento delle modifiche angolari viene eseguito ogni volta che si verifica un evento di ridimensionamento. Ad esempio, la limitazione non influisce sulla frequenza con cui viene eseguito il rilevamento del rilevamento. (Tobias Bosch lo ha confermato: https://github.com/angular/angular/issues/1773#issuecomment-102078250 .)

Voglio solo il rilevamento del cambiamento da eseguire se l’evento passa il tempo di accelerazione. E ho solo bisogno di rilevamento delle modifiche per l’esecuzione su questo componente. La soluzione è creare l’osservabile all’esterno della zona Angolare, quindi chiamare manualmente il rilevamento delle modifiche all’interno della richiamata dell’abbonamento:

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

Ecco un plunker funzionante.

Nel plunker ho aggiunto un counter che incremento ogni ciclo di rilevamento dei cambiamenti usando il ciclo di vita hook ngDoCheck() . Puoi vedere che questo metodo non viene chiamato – il valore del contatore non cambia sugli eventi di ridimensionamento.

detectChanges() eseguirà il rilevamento delle modifiche su questo componente e sui suoi figli. Se si preferisce eseguire il rilevamento delle modifiche dal componente root (ad esempio, eseguire un controllo completo del rilevamento delle modifiche), utilizzare invece ApplicationRef.tick() (questo è commentato nel plunker). Nota che tick() farà sì che ngDoCheck() venga chiamato.


Questa è una grande domanda. Ho passato molto tempo a provare soluzioni diverse e ho imparato molto. Grazie per aver postato questa domanda.

Altro modo in cui ho usato per risolvere questo:

 import { Component, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'your-seelctor', template: 'your-template', }) export class YourComponent{ constructor(public cdRef:ChangeDetectorRef) { } ngAfterViewInit() { this.cdRef.detectChanges(); } }