index - ngfor angular 5
NgFor no actualiza datos con Pipe en Angular2 (8)
En este escenario, estoy mostrando una lista de estudiantes (matriz) a la vista con
ngFor
:
<li *ngFor="#student of students">{{student.name}}</li>
Es maravilloso que se actualice cada vez que agrego otro estudiante a la lista.
Sin embargo, cuando le doy una
pipe
para
filter
por el nombre del alumno,
<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
No actualiza la lista hasta que escribo algo en el campo de filtrado del nombre del alumno.
Aquí hay un enlace a plnkr .
Hello_world.html
<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>
sort_by_name_pipe.ts
import {Pipe} from ''angular2/core'';
@Pipe({
name: ''sortByName''
})
export class SortByNamePipe {
transform(value, [queryString]) {
// console.log(value, queryString);
return value.filter((student) => new RegExp(queryString).test(student.name))
// return value;
}
}
Agregue al parámetro adicional de tubería y cámbielo justo después del cambio de matriz, e incluso con tubería pura, la lista se actualizará
dejar elemento de elementos | tubo: param
Como señaló Eric Martínez en los comentarios, agregar
pure: false
a su decorador
Pipe
y
changeDetection: ChangeDetectionStrategy.OnPush
a su decorador
Component
solucionará su problema.
Aquí hay un plunkr que funciona.
Cambiar a
ChangeDetectionStrategy.Always
.
ChangeDetectionStrategy.Always
funciona también.
Este es el por qué.
Según la guía angular2 en tuberías :
Las tuberías no tienen estado por defecto. Debemos declarar que una tubería tiene estado estableciendo la propiedad
pure
del decorador@Pipe
enfalse
. Esta configuración le dice al sistema de detección de cambios de Angular que verifique la salida de esta tubería en cada ciclo, ya sea que su entrada haya cambiado o no.
En cuanto a
ChangeDetectionStrategy
, de forma predeterminada, todos los enlaces se verifican en cada ciclo.
Cuando se agrega
pure: false
tubería
pure: false
, creo que el método de detección de cambio cambia de
CheckAlways
a
CheckOnce
por razones de rendimiento.
Con
OnPush
, los enlaces para el Componente solo se verifican cuando cambia una propiedad de entrada o cuando se activa un evento.
Para obtener más información sobre los detectores de cambio, una parte importante de
angular2
, consulte los siguientes enlaces:
De la documentación angular
Tubos puros e impuros
Hay dos categorías de tuberías: puras e impuras. Las tuberías son puras por defecto. Cada pipa que has visto hasta ahora ha sido pura. Hace que una tubería sea impura al establecer su bandera pura en falsa. Podría hacer que FlyingHeroesPipe sea impuro de esta manera:
@Pipe({ name: ''flyingHeroesImpure'', pure: false })
Antes de hacerlo, comprenda la diferencia entre puro e impuro, comenzando con un tubo puro.
Tuberías puras Angular ejecuta una tubería pura solo cuando detecta un cambio puro en el valor de entrada. Un cambio puro es un cambio en un valor de entrada primitivo (String, Number, Boolean, Symbol) o una referencia de objeto modificada (Fecha, Array, Función, Objeto).
Angular ignora los cambios dentro de los objetos (compuestos). No llamará a una tubería pura si cambia un mes de entrada, lo agrega a una matriz de entrada o actualiza una propiedad de objeto de entrada.
Esto puede parecer restrictivo pero también es rápido. Una verificación de referencia de objeto es rápida, mucho más rápida que una verificación profunda de diferencias, por lo que Angular puede determinar rápidamente si puede omitir tanto la ejecución de la tubería como una actualización de vista.
Por esta razón, es preferible una tubería pura cuando puede vivir con la estrategia de detección de cambios. Cuando no puede, puede usar la tubería impura.
En este caso de uso, utilicé mi Pipe en el archivo ts para el filtrado de datos. Es mucho mejor para el rendimiento, que usar tuberías puras. Usar en ts como este:
import { YourPipeComponentName } from ''YourPipeComponentPath'';
class YourService {
constructor(private pipe: YourPipeComponentName) {}
YourFunction(value) {
this.pipe.transform(value, ''pipeFilter'');
}
}
En lugar de hacer puro: falso. Puede copiar en profundidad y reemplazar el valor en el componente por this.students = Object.assign ([], NEW_ARRAY); donde NEW_ARRAY es la matriz modificada.
Funciona para angular 6 y debería funcionar también para otras versiones angulares.
Para comprender completamente el problema y las posibles soluciones, debemos analizar la detección de cambios angulares, para tuberías y componentes.
Detección de cambio de tubería
Tubos sin estado / puros
Por defecto, las tuberías son apátridas / puras.
Las tuberías sin estado / puras simplemente transforman los datos de entrada en datos de salida.
No recuerdan nada, por lo que no tienen ninguna propiedad, solo un método
transform()
.
Por lo tanto, Angular puede optimizar el tratamiento de tuberías sin estado / puras: si sus entradas no cambian, las tuberías no necesitan ejecutarse durante un ciclo de detección de cambios.
Para una tubería como
{{power | exponentialStrength: factor}}
{{power | exponentialStrength: factor}}
,
power
y
factor
son entradas.
Para esta pregunta,
"#student of students | sortByName:queryElem.value"
,
students
y
queryElem.value
son entradas y pipe
sortByName
tiene estado / es puro.
students
es una matriz (referencia).
-
Cuando se agrega un estudiante, la
referencia de
matriz no cambia, los
students
no cambian, por lo tanto, la tubería sin estado / pura no se ejecuta. -
Cuando se escribe algo en la entrada del filtro,
queryElem.value
cambia, por lo tanto, se ejecuta la tubería sin estado / pura.
Una forma de solucionar el problema de la matriz es cambiar la referencia de la matriz cada vez que se agrega un alumno, es decir, crear una nueva matriz cada vez que se agrega un alumno.
Podríamos hacer esto con
concat()
:
this.students = this.students.concat([{name: studentName}]);
Aunque esto funciona, nuestro método
addNewStudent()
no debería tener que implementarse de cierta manera solo porque estamos usando una tubería.
Queremos usar
push()
para agregar a nuestra matriz.
Tubos con estado
Las tuberías con estado tienen estado: normalmente tienen propiedades, no solo un método
transform()
.
Es posible que deban evaluarse incluso si sus entradas no han cambiado.
Cuando especificamos que una tubería tiene estado / no es pura,
pure: false
, entonces cada vez que el sistema de detección de cambios de Angular verifica un componente en busca de cambios y ese componente usa una tubería con estado, verificará la salida de la tubería, si su entrada ha cambiado o no.
Esto suena como lo que queremos, aunque es menos eficiente, ya que queremos que la tubería se ejecute incluso si la referencia de los
students
no ha cambiado.
Si simplemente hacemos que la tubería tenga estado, obtenemos un error:
EXCEPTION: Expression ''students | sortByName:queryElem.value in HelloWorld@7:6''
has changed after it was checked. Previous value: ''[object Object],[object Object]''.
Current value: ''[object Object],[object Object]'' in [students | sortByName:queryElem.value
Según
la respuesta de @ drewmoore
, "este error solo ocurre en el modo de desarrollo (que está habilitado de forma predeterminada a partir de beta-0). Si llama a
enableProdMode()
al arrancar la aplicación, el error no se generará".
Los
documentos para
ApplicationRef.tick()
indican:
En el modo de desarrollo, tick () también realiza un segundo ciclo de detección de cambios para garantizar que no se detecten más cambios. Si se recogen cambios adicionales durante este segundo ciclo, los enlaces en la aplicación tienen efectos secundarios que no se pueden resolver en una sola pasada de detección de cambios. En este caso, Angular arroja un error, ya que una aplicación Angular solo puede tener un pase de detección de cambio durante el cual debe completarse toda la detección de cambio.
En nuestro escenario, creo que el error es falso / engañoso. Tenemos una tubería con estado, y la salida puede cambiar cada vez que se llama, puede tener efectos secundarios y está bien. NgFor se evalúa después de la tubería, por lo que debería funcionar bien.
Sin embargo, no podemos desarrollar realmente con este error, por lo que una solución alternativa es agregar una propiedad de matriz (es decir, estado) a la implementación de la tubería y siempre devolver esa matriz. Vea la respuesta de @ pixelbits para esta solución.
Sin embargo, podemos ser más eficientes y, como veremos, no necesitaremos la propiedad de matriz en la implementación de la tubería, y no necesitaremos una solución alternativa para la detección de doble cambio.
Detección de cambio de componentes
De manera predeterminada, en cada evento del navegador, la detección de cambio angular pasa por cada componente para ver si cambió: se verifican las entradas y las plantillas (y tal vez otras cosas).
Si sabemos que un componente solo depende de sus propiedades de entrada (y eventos de plantilla), y que las propiedades de entrada son inmutables, podemos usar la estrategia de detección de cambios
onPush
mucho más eficiente.
Con esta estrategia, en lugar de verificar cada evento del navegador, un componente se verifica solo cuando cambian las entradas y cuando se activan los eventos de la plantilla.
Y, aparentemente, no obtenemos que la
Expression ... has changed after it was checked
error con esta configuración.
Esto se debe a que un componente
onPush
no se vuelve a verificar hasta que se "marca" (
ChangeDetectorRef.markForCheck()
) nuevamente.
Por lo tanto, los enlaces de plantilla y las salidas de tubería con estado se ejecutan / evalúan solo una vez.
Las tuberías sin estado / puras aún no se ejecutan a menos que cambien sus entradas.
Entonces todavía necesitamos una tubería con estado aquí.
Esta es la solución que sugirió @EricMartinez: canalización con estado con detección de cambio
onPush
.
Vea la respuesta de @caffinatedmonkey para esta solución.
Tenga en cuenta que con esta solución, el método
transform()
no necesita devolver la misma matriz cada vez.
Sin embargo, me parece un poco extraño: una tubería con estado sin estado.
Pensando en ello un poco más ... la tubería con estado probablemente siempre debería devolver la misma matriz.
De lo contrario, solo podría usarse con componentes
onPush
en modo dev.
Entonces, después de todo eso, creo que me gusta una combinación de las respuestas de @ Eric y @ pixelbits: canal con estado que devuelve la misma referencia de matriz, con detección de cambio
onPush
si el componente lo permite.
Dado que la tubería con estado devuelve la misma referencia de matriz, la tubería aún se puede usar con componentes que no están configurados con
onPush
.
Esto probablemente se convertirá en un idioma de Angular 2: si una matriz está alimentando una tubería, y la matriz puede cambiar (los elementos en la matriz que es, no la referencia de la matriz), debemos usar una tubería con estado.
Una solución alternativa: importe manualmente Pipe en el constructor y llame al método de transformación utilizando esta tubería
constructor(
private searchFilter : TableFilterPipe) { }
onChange() {
this.data = this.searchFilter.transform(this.sourceData, this.searchText)}
En realidad ni siquiera necesitas una pipa
No necesita cambiar ChangeDetectionStrategy. Implementar una tubería con estado es suficiente para que todo funcione.
Esta es una tubería con estado (no se realizaron otros cambios):
@Pipe({
name: ''sortByName'',
pure: false
})
export class SortByNamePipe {
tmp = [];
transform (value, [queryString]) {
this.tmp.length = 0;
// console.log(value, queryString);
var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
for (var i =0; i < arr.length; ++i) {
this.tmp.push(arr[i]);
}
return this.tmp;
}
}