NgFor ne met pas à jour les données avec Pipe dans Angular2

114

Dans ce scénario, j'affiche une liste d'étudiants (tableau) dans la vue avec ngFor:

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

C'est merveilleux qu'il se mette à jour chaque fois que j'ajoute un autre étudiant à la liste.

Cependant, lorsque je lui donne un pipeà filterpar le nom de l'élève,

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

Il ne met pas à jour la liste tant que je n'ai pas tapé quelque chose dans le champ de filtrage du nom de l'élève.

Voici un lien vers 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
la source
14
Ajoutez pure:falsevotre Pipe et changeDetection: ChangeDetectionStrategy.OnPushvotre Component.
Eric Martinez
2
Merci @EricMartinez. Ça marche. Mais pouvez-vous expliquer un peu?
Chu Son
2
En outre, je suggérerais de ne PAS utiliser .test()dans votre fonction de filtre. C'est parce que, si l'utilisateur entre une chaîne qui comprend des caractères spéciaux comme: *ou +etc., votre code se cassera. Je pense que vous devriez utiliser .includes()ou échapper une chaîne de requête avec une fonction personnalisée.
Eggy
6
Ajouter pure:falseet rendre votre pipe avec état résoudra le problème. La modification de ChangeDetectionStrategy n'est pas nécessaire.
pixelbits
2
Pour tous ceux qui lisent ceci, la documentation de Angular Pipes s'est beaucoup améliorée et reprend plusieurs des mêmes choses discutées ici. Vérifiez-le.
0xcaff

Réponses:

154

Pour bien comprendre le problème et les solutions possibles, nous devons discuter de la détection de changement angulaire - pour les tuyaux et les composants.

Détection de changement de tuyau

Tuyaux sans état / purs

Par défaut, les tubes sont sans état / purs. Les tuyaux sans état / purs transforment simplement les données d'entrée en données de sortie. Ils ne se souviennent de rien, donc ils n'ont aucune propriété - juste une transform()méthode. Angular peut donc optimiser le traitement des tuyaux sans état / purs: si leurs entrées ne changent pas, les tuyaux n'ont pas besoin d'être exécutés lors d'un cycle de détection de changement. Pour un tuyau tel que {{power | exponentialStrength: factor}}, poweret factorsont des entrées.

Pour cette question, "#student of students | sortByName:queryElem.value", studentset queryElem.valuesont entrées, et le tuyau sortByNameest sans état / pur. studentsest un tableau (référence).

  • Lorsqu'un étudiant est ajouté, la référence du tableau ne change pas - studentsne change pas - donc le tube sans état / pur n'est pas exécuté.
  • Quand quelque chose est tapé dans l'entrée du filtre, queryElem.valuechange, donc le tube sans état / pur est exécuté.

Une façon de résoudre le problème du tableau est de changer la référence du tableau chaque fois qu'un étudiant est ajouté - c'est-à-dire de créer un nouveau tableau chaque fois qu'un étudiant est ajouté. Nous pourrions le faire avec concat():

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

Bien que cela fonctionne, notre addNewStudent()méthode ne devrait pas avoir à être implémentée d'une certaine manière simplement parce que nous utilisons un tube. Nous voulons utiliser push()pour ajouter à notre tableau.

Tuyaux avec état

Les tuyaux avec état ont un état - ils ont normalement des propriétés, pas seulement une transform()méthode. Ils peuvent avoir besoin d'être évalués même si leurs entrées n'ont pas changé. Lorsque nous spécifions qu'un tube est avec état / non pur - pure: false- alors chaque fois que le système de détection de changement d'Angular vérifie un composant pour les modifications et que ce composant utilise un tube avec état, il vérifie la sortie du tube, si son entrée a changé ou non.

Cela ressemble à ce que nous voulons, même si c'est moins efficace, car nous voulons que le tube s'exécute même si la studentsréférence n'a pas changé. Si nous rendons simplement le tube avec état, nous obtenons une erreur:

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

Selon la réponse de @ drewmoore , "cette erreur se produit uniquement en mode dev (qui est activé par défaut à partir de la version bêta-0). Si vous appelez enableProdMode()lors du démarrage de l'application, l'erreur ne sera pas levée." La documentation pour l'ApplicationRef.tick() état:

En mode développement, tick () effectue également un deuxième cycle de détection de changement pour s'assurer qu'aucun autre changement n'est détecté. Si des modifications supplémentaires sont prises en compte au cours de ce deuxième cycle, les liaisons dans l'application ont des effets secondaires qui ne peuvent pas être résolus en une seule passe de détection des modifications. Dans ce cas, Angular renvoie une erreur, car une application Angular ne peut avoir qu'une seule passe de détection de changement pendant laquelle toute la détection de changement doit se terminer.

Dans notre scénario, je pense que l'erreur est fausse / trompeuse. Nous avons un tube avec état, et la sortie peut changer chaque fois qu'elle est appelée - cela peut avoir des effets secondaires et ce n'est pas grave. NgFor est évalué après le tube, donc cela devrait fonctionner correctement.

Cependant, nous ne pouvons pas vraiment développer avec cette erreur lancée, donc une solution de contournement consiste à ajouter une propriété de tableau (c'est-à-dire, un état) à l'implémentation du tube et à toujours renvoyer ce tableau. Voir la réponse de @ pixelbits pour cette solution.

Cependant, nous pouvons être plus efficaces, et comme nous le verrons, nous n'aurons pas besoin de la propriété array dans l'implémentation du tube, et nous n'aurons pas besoin d'une solution de contournement pour la détection de double changement.

Détection de changement de composant

Par défaut, à chaque événement du navigateur, la détection de changement angulaire passe par chaque composant pour voir s'il a changé - les entrées et les modèles (et peut-être d'autres choses?) Sont vérifiés.

Si nous savons qu'un composant ne dépend que de ses propriétés d'entrée (et des événements de modèle), et que les propriétés d'entrée sont immuables, nous pouvons utiliser la onPushstratégie de détection des changements beaucoup plus efficace . Avec cette stratégie, au lieu de vérifier chaque événement du navigateur, un composant est vérifié uniquement lorsque les entrées changent et lorsque les événements de modèle se déclenchent. Et, apparemment, nous n'obtenons pas cette Expression ... has changed after it was checkederreur avec ce paramètre. Cela est dû au fait qu'un onPushcomposant n'est pas vérifié à nouveau tant qu'il n'est pas à nouveau "marqué" ( ChangeDetectorRef.markForCheck()). Ainsi, les liaisons de modèle et les sorties de canal avec état ne sont exécutées / évaluées qu'une seule fois. Les tuyaux sans état / purs ne sont toujours pas exécutés à moins que leurs entrées ne changent. Nous avons donc encore besoin d'un tube avec état ici.

C'est la solution proposée par @EricMartinez: pipe avec état avec onPushdétection de changement. Voir la réponse de @ caffinatedmonkey pour cette solution.

Notez qu'avec cette solution, la transform()méthode n'a pas besoin de renvoyer le même tableau à chaque fois. Je trouve cela un peu étrange cependant: un tube avec état sans état. En y réfléchissant un peu plus ... le tube avec état devrait probablement toujours renvoyer le même tableau. Sinon, il ne peut être utilisé qu'avec des onPushcomposants en mode développement.


Donc après tout ça, je pense que j'aime une combinaison des réponses de @ Eric et @ pixelbits: pipe avec état qui renvoie la même référence de tableau, avec onPushdétection de changement si le composant le permet. Étant donné que le tube avec état renvoie la même référence de tableau, le tube peut toujours être utilisé avec des composants qui ne sont pas configurés avec onPush.

Plunker

Cela deviendra probablement un idiome Angular 2: si un tableau alimente un tube et que le tableau peut changer (les éléments du tableau qui sont, pas la référence du tableau), nous devons utiliser un tube avec état.

Mark Rajcok
la source
Merci pour votre explication détaillée du problème. Il est cependant étrange que la détection des modifications des tableaux soit implémentée en tant qu'identité au lieu d'une comparaison d'égalité. Serait-il possible de résoudre ce problème avec Observable?
Martin Nowak
@MartinNowak, si vous demandez si le tableau était un observable, et le tube était sans état ... Je ne sais pas, je n'ai pas essayé ça.
Mark Rajcok le
Une autre solution serait un canal pur où le tableau de sortie suit les modifications dans le tableau d'entrée avec des écouteurs d'événements.
Tuupertunut
Je viens bifurqué votre Plunker, et il fonctionne pour moi sans onPushdétection de changement plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan
28

Comme l'a souligné Eric Martinez dans les commentaires, l'ajout pure: falseà votre Pipedécorateur et changeDetection: ChangeDetectionStrategy.OnPushà votre Componentdécorateur résoudra votre problème. Voici un plunkr fonctionnel. Changer en ChangeDetectionStrategy.Always, fonctionne également. Voici pourquoi.

Selon le guide angular2 sur les tuyaux :

Les tuyaux sont sans état par défaut. Nous devons déclarer un tube avec état en définissant la purepropriété du @Pipedécorateur sur false. Ce paramètre indique au système de détection de changement d'Angular de vérifier la sortie de ce tuyau à chaque cycle, si son entrée a changé ou non.

En ce qui concerne le ChangeDetectionStrategy, par défaut, toutes les liaisons sont vérifiées à chaque cycle. Lorsqu'un pure: falsetuyau est ajouté, je pense que la méthode de détection des modifications passe de CheckAlwaysà CheckOncepour des raisons de performances. Avec OnPush, les liaisons du composant ne sont vérifiées que lorsqu'une propriété d'entrée change ou lorsqu'un événement est déclenché. Pour plus d'informations sur les détecteurs de changement, une partie importante de angular2, consultez les liens suivants:

0xcaff
la source
«Lorsqu'un pure: falsetube est ajouté, je crois que la méthode de détection des modifications passe de CheckAlways à CheckOnce» - cela ne correspond pas à ce que vous avez cité dans la documentation. Ma compréhension est la suivante: les entrées d'un tuyau sans état sont vérifiées à chaque cycle par défaut. S'il y a un changement, la transform()méthode du tube est appelée pour (re) générer la sortie. La transform()méthode d'un tube avec état est appelée à chaque cycle et sa sortie est vérifiée pour les modifications. Voir également stackoverflow.com/a/34477111/215945 .
Mark Rajcok
@MarkRajcok, Oups, vous avez raison, mais si tel est le cas, pourquoi changer la stratégie de détection des changements fonctionne-t-il?
0xcaff
1
Excellente question. Il semblerait que lorsque la stratégie de détection de changement est modifiée en OnPush(c'est- à -dire que le composant est marqué comme "immuable"), la sortie du tube avec état n'a pas à "stabiliser" - c'est-à-dire qu'il semble que la transform()méthode ne soit exécutée qu'une seule fois (probablement toutes la détection des modifications pour le composant n'est exécutée qu'une seule fois). Comparez cela à la réponse de @ pixelbits, où la transform()méthode est appelée plusieurs fois et qu'elle doit se stabiliser (selon l' autre réponse de pixelbits ), d'où la nécessité d'utiliser la même référence de tableau.
Mark Rajcok
@MarkRajcok Si ce que vous dites est correct, en termes d'efficacité, c'est probablement la meilleure façon, dans ce cas d'utilisation particulier, car elle transformn'est appelée que lorsque de nouvelles données sont poussées au lieu de plusieurs fois jusqu'à ce que la sortie se stabilise, non?
0xcaff
1
Probablement, voyez ma réponse interminable. Je souhaite qu'il y ait plus de documentation sur la détection des changements dans Angular 2.
Mark Rajcok
22

Démo Plunkr

Vous n'avez pas besoin de modifier la ChangeDetectionStrategy. L'implémentation d'un Pipe avec état suffit pour que tout fonctionne.

Ceci est un tube avec état (aucune autre modification n'a été apportée):

@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
la source
J'ai eu cette erreur sans modifier changeDectection en onPush: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview unity ( 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
Pouvez-vous expliquer pourquoi vous devez stocker des valeurs dans une variable locale dans SortByNamePipe, puis la renvoyer dans la fonction de transformation @pixelbits? J'ai également remarqué que le cycle angulaire à travers la fonction de transformation deux fois avec changeDetetion.OnPush et 4 fois sans lui
Chu Son
@ChuSon, vous regardez probablement les journaux d'une version précédente. Au lieu de renvoyer un tableau de valeurs, nous renvoyons une référence à un tableau de valeurs, qui peuvent être détectées, je crois. Pixelbits, votre réponse a plus de sens.
0xcaff
Pour le bénéfice des autres lecteurs, concernant l'erreur ChuSon mentionnée ci-dessus dans les commentaires, et la nécessité d'utiliser une variable locale, une autre question a été créée et répondue par pixelbits.
Mark Rajcok
19

De la documentation angulaire

Tuyaux purs et impurs

Il existe deux catégories de tuyaux: purs et impurs. Les tuyaux sont purs par défaut. Chaque pipe que vous avez vue jusqu'à présent est pure. Vous rendez un tuyau impur en définissant son drapeau pur sur false. Vous pourriez rendre le FlyingHeroesPipe impur comme ceci:

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

Avant de faire cela, comprenez la différence entre pur et impur, en commençant par une pipe pure.

Pure pipes Angular exécute un pur tube uniquement lorsqu'il détecte un changement pur de la valeur d'entrée. Un changement pur est soit une modification d'une valeur d'entrée primitive (chaîne, nombre, booléen, symbole) ou une référence d'objet modifiée (date, tableau, fonction, objet).

Angular ignore les changements dans les objets (composites). Il n'appellera pas de canal pur si vous modifiez un mois d'entrée, ajoutez à un tableau d'entrée ou mettez à jour une propriété d'objet d'entrée.

Cela peut sembler restrictif mais c'est aussi rapide. Une vérification de référence d'objet est rapide - beaucoup plus rapide qu'une vérification approfondie des différences - donc Angular peut rapidement déterminer s'il peut ignorer à la fois l'exécution du tuyau et une mise à jour de vue.

Pour cette raison, un tuyau pur est préférable lorsque vous pouvez vivre avec la stratégie de détection des changements. Lorsque vous ne pouvez pas, vous pouvez utiliser le tuyau impur.

Sumit Jaiswal
la source
La réponse est correcte, mais un peu plus d'effort lors de la publication, ce serait bien
Stefan
2

Au lieu de faire pur: faux. Vous pouvez copier en profondeur et remplacer la valeur du composant par this.students = Object.assign ([], NEW_ARRAY); où NEW_ARRAY est le tableau modifié.

Cela fonctionne pour angular 6 et devrait également fonctionner pour d'autres versions angulaires.

ideeps
la source
0

Solution de contournement: importez manuellement le tuyau dans le constructeur et appelez la méthode de transformation à l'aide de ce tuyau

constructor(
private searchFilter : TableFilterPipe) { }

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

En fait, tu n'as même pas besoin de pipe

Richard Luo
la source
0

Ajoutez un paramètre supplémentaire au tube et modifiez-le juste après le changement de tableau, et même avec un tube pur, la liste sera actualisée

laisser un article | tuyau: paramètre

Nick Bistrov
la source
0

Dans ce cas d'utilisation, j'ai utilisé mon fichier Pipe in ts pour le filtrage des données. C'est bien meilleur pour les performances que d'utiliser des tuyaux purs. Utilisez dans ts comme ceci:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

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

créer des tuyaux impurs coûte cher en performance. donc ne créez pas de tuyaux impurs, modifiez plutôt la référence de la variable de données en créant une copie des données lors du changement de données et réaffectez la référence de copie dans la variable de données d'origine.

            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
la source