javascript - parametros - Angular2: la expresión ha cambiado después de que se comprobó: vinculación al ancho de div con eventos de cambio de tamaño
viewchild angular 4 example (2)
He realizado algunas lecturas e investigaciones sobre este error, pero no estoy seguro de cuál es la respuesta correcta para mi situación. Entiendo que en el modo dev, la detección de cambios se ejecuta dos veces, pero soy reacio a usar enableProdMode()
para enmascarar el problema.
Aquí hay un ejemplo simple donde la cantidad de celdas en la tabla debería aumentar a medida que el ancho del div se expande. (Tenga en cuenta que el ancho del div no es una función del ancho de la pantalla, por lo que @Media no se puede aplicar fácilmente)
Mi HTML se ve de la siguiente manera (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>
Esto por sí solo no hace nada. Supongo que esto se debe a que nada causa la detección de cambios. Sin embargo, cuando cambio la primera línea a la siguiente, y creo una función vacía para recibir la llamada, comienza a funcionar, pero ocasionalmente aparece la ''Expresión ha cambiado después de que se comprobó el error''.
<div #widgetParentDiv class="Content">
gets replaced with
<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
Mi mejor suposición es que con esta modificación, se activa la detección de cambios y todo comienza a responder, sin embargo, cuando el ancho cambia rápidamente, se lanza la excepción porque la iteración anterior de la detección de cambios tomó más tiempo para completar que cambiar el ancho del div.
- ¿Hay un mejor enfoque para activar la detección de cambios?
- ¿Debería capturar el evento de cambio de tamaño a través de una función para asegurar que ocurra la detección de cambio?
- Está usando #widthParentDiv para acceder al ancho del div aceptable?
- ¿Hay una mejor solución general?
Para más detalles sobre mi proyecto, vea this pregunta similar.
Gracias
Otra forma en que solía resolver esto:
import { Component, ChangeDetectorRef } from ''@angular/core'';
@Component({
selector: ''your-seelctor'',
template: ''your-template'',
})
export class YourComponent{
constructor(public cdRef:ChangeDetectorRef) { }
ngAfterViewInit() {
this.cdRef.detectChanges();
}
}
Para resolver su problema, simplemente necesita obtener y almacenar el tamaño del div
en una propiedad del componente después de cada evento de cambio de tamaño, y usar esa propiedad en la plantilla. De esta forma, el valor permanecerá constante cuando la segunda ronda de detección de cambios se ejecute en modo dev.
También recomiendo usar @HostListener
lugar de agregar (window:resize)
a su plantilla. Usaremos @ViewChild
para obtener una referencia al div
. Y usaremos el ciclo de vida hook ngAfterViewInit()
para establecer el valor inicial.
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;
}
}
Lástima que no funciona. Obtenemos
La expresión ha cambiado después de que se verificó. Valor anterior: ''falso''. Valor actual: ''verdadero''.
El error es quejarse de nuestras expresiones NgIf
: la primera vez que se ejecuta, divWidth
es 0, luego ngAfterViewInit()
ejecuta y cambia el valor a algo distinto de 0, luego se ejecuta la segunda ronda de detección de cambios (en modo dev). Afortunadamente, hay una solución fácil / conocida, y esta es una cuestión de una sola vez, no un problema continuo como en el PO:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
Tenga en cuenta que esta técnica, de esperar un tic, está documentada aquí: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
A menudo, en ngAfterViewInit()
y ngAfterViewChecked()
tendremos que emplear el truco setTimeout()
porque estos métodos se setTimeout()
después de que se compuso la vista del componente.
Aquí hay un plunker funciona.
Podemos hacer esto mejor. Creo que deberíamos reducir el tamaño de los eventos de manera que la detección de cambio angular solo se ejecute, por ejemplo, cada 100-250 ms, en lugar de cada vez que ocurre un evento de cambio de tamaño. Esto debería evitar que la aplicación se vuelva lenta cuando el usuario cambia el tamaño de la ventana, porque en este momento, cada evento de cambio de tamaño hace que se ejecute la detección de cambio (dos veces en el modo dev). Puede verificar esto agregando el siguiente método al plunker anterior:
ngDoCheck() {
console.log(''change detection'');
}
Los observables pueden acelerar fácilmente los eventos, así que en lugar de usar @HostListener
para enlazar al evento de cambio de tamaño, crearemos un observable:
Observable.fromEvent(window, ''resize'')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
Esto funciona, pero ... al experimentar con eso, descubrí algo muy interesante ... aunque aceleramos el cambio de tamaño del evento, la detección de cambio angular sigue ejecutándose cada vez que hay un evento de cambio de tamaño. Es decir, la aceleración no afecta la frecuencia con la que se ejecuta la detección de cambios. (Tobias Bosch confirmó esto: https://github.com/angular/angular/issues/1773#issuecomment-102078250 .)
Solo quiero que la detección de cambio se ejecute si el evento pasa el tiempo del acelerador. Y solo necesito la detección de cambios para ejecutar en este componente. La solución es crear el observable fuera de la zona angular, luego llamar manualmente la detección de cambios dentro de la devolución de llamada de suscripción:
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();
})
);
}
Aquí hay un plunker funciona.
En el plunker agregué un counter
que incrementó cada ciclo de detección de cambio usando el ciclo de vida hook ngDoCheck()
. Puede ver que no se está llamando a este método: el valor del contador no cambia al cambiar el tamaño de los eventos.
detectChanges()
ejecutará la detección de cambios en este componente y sus elementos detectChanges()
. Si prefieres ejecutar la detección de cambios desde el componente raíz (es decir, ejecutar una comprobación de detección de cambios completa) entonces utiliza ApplicationRef.tick()
lugar (esto está comentado en el plunker). Tenga en cuenta que tick()
hará que se ngDoCheck()
.
Esta es una gran pregunta. Pasé mucho tiempo probando diferentes soluciones y aprendí mucho. Gracias por publicar esta pregunta.