NgFor no actualiza los datos con Pipe en Angular2

114

En este escenario, estoy mostrando una lista de estudiantes (matriz) en la vista con ngFor:

<li *ngFor="#student of students">{{student.name}}</li>

Es maravilloso que se actualice cada vez que agrego a otro estudiante a la lista.

Sin embargo, cuando le doy una pipea filterpor el nombre del estudiante,

<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 estudiante.

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

Chu Son
fuente
14
Agregue pure:falsesu tubería y changeDetection: ChangeDetectionStrategy.OnPushsu componente.
Eric Martinez
2
Gracias @EricMartinez. Funciona. ¿Pero puedes explicar un poco?
Chu Son
2
Además, sugeriría NO usar .test()en su función de filtro. Es porque, si el usuario ingresa una cadena que incluye caracteres de significado especial como: *o +etc., su código se romperá. Creo que debería usar .includes()o escapar de la cadena de consulta con una función personalizada.
Eggy
6
Agregar pure:falsey hacer que su tubería tenga estado solucionará el problema. No es necesario modificar ChangeDetectionStrategy.
pixelbits
2
Para cualquiera que lea esto, la documentación de Angular Pipes ha mejorado mucho y repasa muchas de las mismas cosas que se analizan aquí. Echale un vistazo.
0xcaff

Respuestas:

154

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

De forma predeterminada, las canalizaciones son sin estado / 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 propiedades, solo un transform()método. 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}}, powery factorson entradas.

Para esta pregunta, "#student of students | sortByName:queryElem.value", studentsy queryElem.valueson entradas, y el tubo sortByNameno tiene estado / puro. studentses una matriz (referencia).

  • Cuando se agrega un estudiante, la referencia de la matriz no cambia, studentsno cambia, por lo tanto, la tubería sin estado / pura no se ejecuta.
  • Cuando se escribe algo en la entrada del filtro, queryElem.valuecambia, 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 estudiante, es decir, crear una nueva matriz cada vez que se agrega un estudiante. Podríamos hacer esto con concat():

this.students = this.students.concat([{name: studentName}]);

Aunque esto funciona, nuestro addNewStudent()método 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 transform()método. Es posible que deban evaluarse incluso si sus entradas no han cambiado. Cuando especificamos que una tubería tiene estado / no es pura, pure: falseentonces siempre que el sistema de detección de cambios de Angular verifique un componente en busca de cambios y ese componente use una tubería con estado, verificará la salida de la tubería, ya sea que su entrada haya 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 studentsreferencia 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

De acuerdo con la respuesta de @ drewmoore , "este error solo ocurre en el modo de desarrollo (que está habilitado de manera predeterminada a partir de la versión beta-0). Si llama enableProdMode()al iniciar la aplicación, el error no se producirá". Los documentos para elApplicationRef.tick() estado:

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, las vinculaciones 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 una pasada de detección de cambios durante la cual deben completarse todas las detecciones de cambios.

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 eso está bien. NgFor se evalúa después de la tubería, por lo que debería funcionar bien.

Sin embargo, realmente no podemos desarrollar con este error, por lo que una solución 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 para la detección de doble cambio.

Detección de cambio de componente

De forma predeterminada, en cada evento del navegador, la detección de cambios angulares pasa por cada componente para ver si cambió: las entradas y plantillas (¿y tal vez otras cosas?) Están marcadas.

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 onPushestrategia de detección de cambios mucho más eficiente . Con esta estrategia, en lugar de verificar cada evento del navegador, un componente se verifica solo cuando las entradas cambian y cuando se activan los eventos de la plantilla. Y, aparentemente, no obtenemos ese Expression ... has changed after it was checkederror con esta configuración. Esto se debe a que un onPushcomponente no se vuelve a comprobar hasta que se vuelve a "marcar" ( ChangeDetectorRef.markForCheck()). Por lo tanto, los enlaces de plantilla y las salidas de tubería con estado se ejecutan / evalúan solo una vez. Las canalizaciones sin estado / puras todavía 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: tubería con estado con onPushdetección de cambios. Vea la respuesta de @caffinatedmonkey para esta solución.

Tenga en cuenta que con esta solución, el transform()método 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 onPushcomponentes en modo dev.


Entonces, después de todo eso, creo que me gusta una combinación de las respuestas de @ Eric y @ pixelbits: tubería con estado que devuelve la misma referencia de matriz, con onPushdetección de cambios 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.

Plunker

Esto probablemente se convertirá en un modismo de Angular 2: si una matriz está alimentando una tubería, y la matriz puede cambiar (los elementos de la matriz, no la referencia de la matriz), necesitamos usar una tubería con estado.

Mark Rajcok
fuente
Gracias por su explicación detallada del problema. Sin embargo, es extraño que la detección de cambios de matrices se implemente como identidad en lugar de comparación de igualdad. ¿Sería posible resolver esto con Observable?
Martin Nowak
@MartinNowak, si está preguntando si la matriz era un Observable y la tubería no tenía estado ... No lo sé, no lo he intentado.
Mark Rajcok
Otra solución sería una tubería pura donde la matriz de salida rastrea los cambios en la matriz de entrada con detectores de eventos.
Tuupertunut
Acabo de bifurcar su Plunker y me funciona sin onPushdetección de cambios plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan
28

Como señaló Eric Martinez en los comentarios, agregar pure: falsea su Pipedecorador y changeDetection: ChangeDetectionStrategy.OnPusha su Componentdecorador solucionará su problema. Aquí hay un plunkr que funciona. Cambiar a ChangeDetectionStrategy.Always, también funciona. Este es el por qué.

Según la guía angular2 sobre tuberías :

Las tuberías no tienen estado de forma predeterminada. Debemos declarar que una tubería tiene estado estableciendo la purepropiedad del @Pipedecorador en false. 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, por defecto, todos los enlaces se comprueban en cada ciclo. Cuando pure: falsese agrega una tubería, creo que el método de detección de cambios cambia de CheckAlwaysa CheckOncepor razones de rendimiento. Con OnPush, los enlaces para el componente solo se comprueban cuando cambia una propiedad de entrada o cuando se desencadena un evento. Para obtener más información sobre los detectores de cambios, una parte importante de angular2, consulte los siguientes enlaces:

0xcaff
fuente
"Cuando pure: falsese agrega una tubería, creo que el método de detección de cambios cambia de CheckAlways a CheckOnce", eso no concuerda con lo que citó de los documentos. Mi comprensión es la siguiente: las entradas de una tubería sin estado se verifican en cada ciclo de forma predeterminada. Si hay un cambio, transform()se llama al método de la tubería para (re) generar la salida. El transform()método de una tubería con estado se llama en cada ciclo y se comprueba su salida para ver si hay cambios. Consulte también stackoverflow.com/a/34477111/215945 .
Mark Rajcok
@MarkRajcok, Vaya, tienes razón, pero si ese es el caso, ¿por qué funciona cambiar la estrategia de detección de cambios?
0xcaff
1
Gran pregunta. Parecería que cuando se cambia la estrategia de detección de cambios a OnPush(es decir, el componente está marcado como "inmutable"), la salida de tubería con estado no tiene que "estabilizarse", es decir, parece que el transform()método se ejecuta solo una vez (probablemente todos la detección de cambios para el componente se ejecuta solo una vez). Contraste eso con la respuesta de @ pixelbits, donde el transform()método se llama varias veces y tiene que estabilizarse (de acuerdo con la otra respuesta de pixelbits ), de ahí la necesidad de usar la misma referencia de matriz.
Mark Rajcok
@MarkRajcok Si lo que dice es correcto, en términos de eficiencia, esta es probablemente la mejor manera, en este caso de uso particular porque transformsolo se llama cuando se envían nuevos datos en lugar de varias veces hasta que la salida se estabiliza, ¿verdad?
0xcaff
1
Probablemente, vea mi respuesta interminable. Ojalá hubiera más documentación sobre la detección de cambios en Angular 2.
Mark Rajcok
22

Demo Plunkr

No es necesario 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;
  }
}
pixelbits
fuente
Recibí este error sin modificar changeDectection a onPush: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview](Plnkr) Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]
Chu Son
1
¿Puede dar un ejemplo de por qué necesita almacenar valores en una variable local en SortByNamePipe y luego devolverlo en la función de transformación @pixelbits? También noté que el ciclo angular a través de la función de transformación dos veces con changeDetetion.OnPush y 4 veces sin ella
Chu Son
@ChuSon, probablemente estés viendo registros de una versión anterior. En lugar de devolver una matriz de valores, estamos devolviendo una referencia a una matriz de valores, que creo que pueden detectarse por cambios. Pixelbits, tu respuesta tiene más sentido.
0xcaff
Para beneficio de otros lectores, con respecto al error ChuSon mencionado anteriormente en los comentarios, y la necesidad de usar una variable local, se creó otra pregunta y fue respondida por pixelbits.
Mark Rajcok
19

De la documentación angular

Pipas puras e impuras

Hay dos categorías de pipas: puras e impuras. Las tuberías son puras por defecto. Cada pipa que has visto hasta ahora ha sido pura. Usted convierte una tubería en impura estableciendo su bandera pura en falso. Podrías hacer que FlyingHeroesPipe sea impuro así:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

Antes de hacer eso, comprenda la diferencia entre puro e impuro, comenzando con una pipa pura.

Tubos puros Angular ejecuta un tubo puro solo cuando detecta un cambio puro en el valor de entrada. Un cambio puro es un cambio en un valor de entrada primitivo (Cadena, Número, Booleano, Símbolo) o una referencia de objeto modificada (Fecha, Matriz, 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 la actualización de la vista.

Por esta razón, es preferible una tubería pura cuando puede vivir con la estrategia de detección de cambios. Cuando no pueda, puede usar la pipa impura.

Sumit Jaiswal
fuente
La respuesta es correcta, pero un poco más de esfuerzo al publicarla sería bueno
Stefan
2

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.

ideeps
fuente
0

Una solución alternativa: importe manualmente la tubería 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

Richard Luo
fuente
0

Agregue un parámetro adicional a la tubería y cámbielo justo después del cambio de matriz, e incluso con tubería pura, la lista se actualizará

dejar artículo de artículos | tubería: param

Nick Bistrov
fuente
0

En este caso de uso, utilicé mi archivo Pipe in ts para el filtrado de datos. Es mucho mejor para el rendimiento que usar tubos puros. Úselo en ts como este:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}
Andris
fuente
0

crear tuberías impuras tiene un alto rendimiento. así que no cree tuberías impuras, más bien cambie la referencia de la variable de datos creando una copia de los datos cuando cambie en los datos y reasigne la referencia de la copia en la variable de datos original.

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
amit bhandari
fuente