NgFor non aggiorna i dati con Pipe in Angular2


114

In questo scenario, sto visualizzando un elenco di studenti (array) nella vista con ngFor:

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

È meraviglioso che si aggiorni ogni volta che aggiungo un altro studente all'elenco.

Tuttavia, quando do un pipea filteraccanto al nome dello studente,

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

Non aggiorna l'elenco fino a quando non digito qualcosa nel campo del nome dello studente da filtrare.

Ecco un collegamento 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;
    }
}


14
Aggiungi pure:falsenel tuo tubo e changeDetection: ChangeDetectionStrategy.OnPushnel tuo componente.
Eric Martinez

2
Grazie @EricMartinez. Funziona. Ma puoi spiegare un po '?
Chu Son

2
Inoltre, suggerirei di NON utilizzare .test()nella funzione di filtro. È perché, se l'utente inserisce una stringa che include caratteri di significato speciale come: *o +ecc. Il codice si interromperà. Penso che dovresti usare .includes()o eseguire l'escape della stringa di query con la funzione personalizzata.
Eggy

6
Aggiungere pure:falsee rendere stateful il tuo pipe risolverà il problema. La modifica di ChangeDetectionStrategy non è necessaria.
pixelbits

2
Per chiunque legga questo, la documentazione per Angular Pipes è migliorata molto e ripercorre molte delle stesse cose discusse qui. Controlla.
0xcaff

Risposte:


154

Per comprendere appieno il problema e le possibili soluzioni, dobbiamo discutere il rilevamento del cambiamento angolare - per tubi e componenti.

Rilevamento cambio tubo

Tubi apolidi / puri

Per impostazione predefinita, le pipe sono senza stato / pure. Le pipe senza stato / pure trasformano semplicemente i dati di input in dati di output. Non ricordano nulla, quindi non hanno proprietà, solo un transform()metodo. Angular può quindi ottimizzare il trattamento dei tubi stateless / puri: se i loro input non cambiano, i tubi non devono essere eseguiti durante un ciclo di rilevamento delle modifiche. Per una pipe come {{power | exponentialStrength: factor}}, powere factorsono input.

Per questa domanda, "#student of students | sortByName:queryElem.value", studentse queryElem.valuesono ingressi, e tubo sortByNameè stateless / puro. studentsè un array (riferimento).

  • Quando viene aggiunto uno studente, il riferimento all'array non cambia students, non cambia, quindi la pipe senza stato / pura non viene eseguita.
  • Quando qualcosa viene digitato nell'input del filtro, queryElem.valuecambia, quindi viene eseguita la pipe senza stato / pura.

Un modo per risolvere il problema dell'array è cambiare il riferimento all'array ogni volta che viene aggiunto uno studente, ovvero creare un nuovo array ogni volta che viene aggiunto uno studente. Potremmo farlo con concat():

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

Sebbene funzioni, il nostro addNewStudent()metodo non dovrebbe essere implementato in un certo modo solo perché stiamo usando una pipe. Vogliamo usare push()per aggiungere al nostro array.

Stateful Pipes

Le pipe con stato hanno uno stato: normalmente hanno proprietà, non solo un transform()metodo. Potrebbe essere necessario valutarli anche se i loro input non sono cambiati. Quando specifichiamo che un pipe è stateful / non puro - pure: false- allora ogni volta che il sistema di rilevamento delle modifiche di Angular controlla un componente per le modifiche e quel componente utilizza un pipe stateful, controllerà l'output del pipe, indipendentemente dal fatto che il suo input sia cambiato o meno.

Questo suona come quello che vogliamo, anche se è meno efficiente, poiché vogliamo che la pipe venga eseguita anche se il studentsriferimento non è cambiato. Se rendiamo semplicemente stateful la pipe, otteniamo un errore:

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

Secondo la risposta di @ drewmoore , "questo errore si verifica solo in modalità dev (che è abilitata per impostazione predefinita a partire dalla beta-0). Se chiami enableProdMode()durante il bootstrap dell'app, l'errore non verrà generato." I documenti per loApplicationRef.tick() stato:

In modalità sviluppo, tick () esegue anche un secondo ciclo di rilevamento delle modifiche per garantire che non vengano rilevate ulteriori modifiche. Se durante questo secondo ciclo vengono rilevate ulteriori modifiche, le associazioni nell'app hanno effetti collaterali che non possono essere risolti in un singolo passaggio di rilevamento delle modifiche. In questo caso, Angular genera un errore, poiché un'applicazione Angular può avere solo un passaggio di rilevamento delle modifiche durante il quale deve essere completato tutto il rilevamento delle modifiche.

Nel nostro scenario credo che l'errore sia fasullo / fuorviante. Abbiamo una pipe con stato e l'output può cambiare ogni volta che viene chiamato - può avere effetti collaterali e va bene. NgFor viene valutato dopo la pipe, quindi dovrebbe funzionare bene.

Tuttavia, non possiamo davvero sviluppare con questo errore generato, quindi una soluzione alternativa è aggiungere una proprietà di array (cioè, stato) all'implementazione del pipe e restituire sempre quell'array. Vedi la risposta di @ pixelbits per questa soluzione.

Tuttavia, possiamo essere più efficienti e, come vedremo, non avremo bisogno della proprietà array nell'implementazione del pipe e non avremo bisogno di una soluzione alternativa per il rilevamento del doppio cambiamento.

Rilevamento del cambio di componente

Per impostazione predefinita, su ogni evento del browser, il rilevamento delle modifiche angolari passa attraverso ogni componente per vedere se è cambiato: vengono controllati input e modelli (e forse altre cose?).

Se sappiamo che un componente dipende solo dalle sue proprietà di input (e dagli eventi del modello) e che le proprietà di input sono immutabili, possiamo utilizzare la onPushstrategia di rilevamento delle modifiche molto più efficiente . Con questa strategia, invece di controllare ogni evento del browser, un componente viene controllato solo quando cambiano gli input e quando si attivano gli eventi del modello. E, a quanto pare, non otteniamo Expression ... has changed after it was checkedquell'errore con questa impostazione. Questo perché un onPushcomponente non viene controllato di nuovo finché non viene "contrassegnato" ( ChangeDetectorRef.markForCheck()) di nuovo. Quindi le associazioni dei modelli e gli output delle pipe con stato vengono eseguiti / valutati solo una volta. Le pipe senza stato / pure non vengono ancora eseguite a meno che i loro input non cambino. Quindi abbiamo ancora bisogno di un tubo con stato qui.

Questa è la soluzione suggerita da @EricMartinez: stateful pipe con onPushrilevamento delle modifiche. Vedi la risposta di @ caffinatedmonkey per questa soluzione.

Nota che con questa soluzione il transform()metodo non deve restituire ogni volta lo stesso array. Lo trovo un po 'strano però: una pipe con stato senza stato. Pensandoci ancora un po '... la pipe con stato probabilmente dovrebbe sempre restituire lo stesso array. Altrimenti potrebbe essere utilizzato solo con onPushcomponenti in modalità dev.


Quindi, dopo tutto ciò, penso che mi piaccia una combinazione delle risposte di @ Eric e @ pixelbits: pipe stateful che restituisce lo stesso riferimento di array, con onPushrilevamento delle modifiche se il componente lo consente. Poiché la pipe con stato restituisce lo stesso riferimento all'array, la pipe può ancora essere utilizzata con componenti che non sono configurati con onPush.

Plunker

Questo probabilmente diventerà un idioma Angular 2: se un array sta alimentando una pipe e l'array potrebbe cambiare (gli elementi nell'array che è, non il riferimento all'array), dobbiamo usare un pipe con stato.


Grazie per la tua spiegazione dettagliata del problema. È strano però che il rilevamento delle modifiche degli array sia implementato come identità anziché come confronto di uguaglianza. Sarebbe possibile risolvere questo problema con Observable?
Martin Nowak,

@MartinNowak, se stai chiedendo se l'array fosse un Observable e il pipe fosse apolidi ... Non lo so, non l'ho provato.
Mark Rajcok

Un'altra soluzione sarebbe una pipe pura in cui l'array di output tiene traccia delle modifiche nell'array di input con i listener di eventi.
Tuupertunut

Ho appena biforcato il tuo Plunker e funziona per me senza onPushrilevamento delle modifiche plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan

28

Come ha sottolineato Eric Martinez nei commenti, l'aggiunta pure: falseal tuo Pipedecoratore e changeDetection: ChangeDetectionStrategy.OnPushal tuo Componentdecoratore risolverà il tuo problema. Ecco un plunkr funzionante. Anche il passaggio a ChangeDetectionStrategy.Always, funziona. Ecco perché.

Secondo la guida angular2 sui tubi :

Le pipe sono senza stato per impostazione predefinita. Dobbiamo dichiarare una pipe come stateful impostando la pureproprietà del @Pipedecoratore su false. Questa impostazione indica al sistema di rilevamento delle modifiche di Angular di controllare l'output di questo tubo ad ogni ciclo, indipendentemente dal fatto che il suo input sia cambiato o meno.

Per quanto riguarda il ChangeDetectionStrategy, di default, tutti i collegamenti vengono controllati ogni singolo ciclo. Quando pure: falseviene aggiunta una pipe, credo che il metodo di rilevamento delle modifiche cambi da da CheckAlwaysa CheckOnceper motivi di prestazioni. Con OnPush, le associazioni per il componente vengono verificate solo quando una proprietà di input cambia o quando viene attivato un evento. Per ulteriori informazioni sui rilevatori di modifiche, una parte importante di angular2, controllare i seguenti collegamenti:


"Quando pure: falseviene aggiunta una pipe, credo che il metodo di rilevamento delle modifiche cambi da CheckAlways a CheckOnce", il che non concorda con ciò che hai citato nei documenti. La mia comprensione è la seguente: gli input di una pipe senza stato vengono controllati ogni ciclo per impostazione predefinita. Se c'è una modifica, il transform()metodo della pipe viene chiamato per (ri) generare l'output. Il transform()metodo di una pipe con stato viene chiamato ogni ciclo e il suo output viene controllato per eventuali modifiche. Vedi anche stackoverflow.com/a/34477111/215945 .
Mark Rajcok

@MarkRajcok, Oops, hai ragione, ma se è così, perché la modifica della strategia di rilevamento delle modifiche funziona?
0xcaff

1
Ottima domanda. Sembrerebbe che quando la strategia di rilevamento delle modifiche viene modificata in OnPush(ovvero, il componente è contrassegnato come "immutabile"), l'output della pipe stateful non deve "stabilizzarsi" - ovvero, sembra che il transform()metodo venga eseguito solo una volta (probabilmente tutto il rilevamento delle modifiche per il componente viene eseguito solo una volta). Confrontalo con la risposta di @ pixelbits, in cui il transform()metodo viene chiamato più volte e deve stabilizzarsi (secondo l' altra risposta di pixelbits ), da qui la necessità di utilizzare lo stesso riferimento di array.
Mark Rajcok

@MarkRajcok Se quello che dici è corretto, in termini di efficienza, questo è probabilmente il modo migliore, in questo particolare caso d'uso perché transformviene chiamato solo quando vengono inviati nuovi dati invece che più volte fino a quando l'output si stabilizza, giusto?
0xcaff

1
Probabilmente, vedi la mia risposta prolissa. Vorrei che ci fosse più documentazione sul rilevamento delle modifiche in Angular 2.
Mark Rajcok

22

Demo Plunkr

Non è necessario modificare ChangeDetectionStrategy. L'implementazione di una pipe con stato è sufficiente per far funzionare tutto.

Questa è una pipe con stato (non sono state apportate altre modifiche):

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

Ho ricevuto questo errore senza alterare changeDectection su 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
Puoi esempio perché devi memorizzare valori in una variabile locale in SortByNamePipe e quindi restituirlo nella funzione di trasformazione @pixelbits? Ho anche notato che Angular passa attraverso la funzione di trasformazione due volte con changeDetetion.OnPush e 4 volte senza di essa
Chu Son

@ChuSon, probabilmente stai guardando i log di una versione precedente. Invece di restituire un array di valori, stiamo restituendo un riferimento a un array di valori, che può essere rilevato dalle modifiche, credo. Pixelbits, la tua risposta ha più senso.
0xcaff

A beneficio di altri lettori, per quanto riguarda l'errore ChuSon menzionato sopra nei commenti e la necessità di utilizzare una variabile locale, è stata creata un'altra domanda e risposta da pixelbits.
Mark Rajcok

19

Dalla documentazione angolare

Tubi puri e impuri

Esistono due categorie di pipe: pure e impure. I tubi sono puri per impostazione predefinita. Ogni pipa che hai visto finora è stata pura. Rendi impuro un tubo impostando la sua bandiera pura su falso. Potresti rendere il FlyingHeroesPipe impuro in questo modo:

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

Prima di farlo, comprendi la differenza tra puro e impuro, iniziando con una pipa pura.

Tubi puri Angular esegue un pipe puro solo quando rileva una modifica pura al valore di input. Una modifica pura è una modifica a un valore di input primitivo (String, Number, Boolean, Symbol) o un riferimento a un oggetto modificato (Date, Array, Function, Object).

Angular ignora i cambiamenti all'interno degli oggetti (compositi). Non chiamerà una pipe pura se si modifica un mese di input, si aggiunge a un array di input o si aggiorna la proprietà di un oggetto di input.

Questo può sembrare restrittivo ma è anche veloce. Un controllo del riferimento a un oggetto è veloce, molto più veloce di un controllo approfondito delle differenze, quindi Angular può determinare rapidamente se può saltare sia l'esecuzione del pipe che un aggiornamento della vista.

Per questo motivo, un pipe puro è preferibile quando puoi convivere con la strategia di rilevamento delle modifiche. Quando non puoi, puoi usare il tubo impuro.


La risposta è corretta, ma un po 'più di impegno durante la pubblicazione sarebbe bello
Stefan

2

Invece di fare puro: falso. Puoi copiare e sostituire il valore nel componente con this.students = Object.assign ([], NEW_ARRAY); dove NEW_ARRAY è l'array modificato.

Funziona per l'angolo 6 e dovrebbe funzionare anche per altre versioni angolari.


0

Una soluzione alternativa: importare manualmente Pipe nel costruttore e chiamare il metodo di trasformazione utilizzando questa pipe

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

In realtà non hai nemmeno bisogno di una pipa


0

Aggiungi a pipe un parametro extra e modificalo subito dopo la modifica dell'array, e anche con pipe puro, l'elenco verrà aggiornato

lasciare articolo di articoli | Tubo: param


0

In questo caso di utilizzo ho utilizzato il mio file Pipe in ts per il filtraggio dei dati. È molto meglio per le prestazioni rispetto all'utilizzo di tubi puri. Usa in ts come questo:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

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

0

creare tubi impuri è costoso in termini di prestazioni. quindi non creare tubi impuri, ma piuttosto modificare il riferimento della variabile dati creando una copia dei dati quando cambiano i dati e riassegnare il riferimento della copia nella variabile dati originale.

            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();
            } 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.