web-dev-qa-db-fra.com

Il n'y a pas de jours à venir

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 que cela mette à jour chaque fois que j'ajoute un autre étudiant à la liste. 

Cependant, quand je lui donne un pipe à filter par le nom de l’étudiant, 

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

La liste n'est pas mise à jour tant que je n'ai pas saisi quelque chose dans le champ du nom de filtrage de l'étudiant.

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

102
Chu Son

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

Détection de changement de tuyau

Pipes sans état/purs

Par défaut, les canaux sont sans état/purs. Les tubes sans état/purs transforment simplement les données d'entrée en données de sortie. Ils ne se souviennent de rien, ils n'ont donc aucune propriété - juste une méthode transform(). 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 pendant un cycle de détection de changement. Pour un canal tel que {{power | exponentialStrength: factor}}, poweret factorsont des entrées.

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

  • Lorsqu'un étudiant est ajouté, la référence au tableau ne change pas - studentsne change pas - par conséquent, le canal sans état/pur n'est pas exécuté.
  • Lorsque quelque chose est tapé dans l'entrée de filtre, queryElem.value change, le tuyau sans état/pur est donc exécuté.

Une façon de résoudre le problème de tableau consiste à modifier la référence du tableau chaque fois qu'un élève est ajouté - c.-à-d., Créez un nouveau tableau chaque fois qu'un élève est ajouté. Nous pourrions faire cela avec concat():

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

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

Pipes Stateful

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

Cela ressemble à ce que nous voulons, même s’il est moins efficace, car nous voulons que le canal s’exécute même si la référence studentsn’a pas changé. Si nous donnons simplement un état à un tuyau, 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 réponse de @ drewmoore , "cette erreur se produit uniquement en mode dev (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 renvoyée. . " Le docs pour ApplicationRef.tick() indique:

En mode de 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 détectées 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 étape de détection de modification. Dans ce cas, Angular renvoie une erreur, car une application Angular ne peut avoir qu'un seul passage de détection de modification au cours duquel toute la détection de modification doit être terminée.

Dans notre scénario, je pense que l'erreur est fausse/trompeuse. Nous avons un canal avec état, et la sortie peut changer à chaque fois qu'il est appelé - cela peut avoir des effets secondaires et c'est bon. NgFor est évalué après le tuyau, il devrait donc fonctionner correctement.

Cependant, nous ne pouvons pas vraiment développer cette erreur, une solution consiste donc à ajouter une propriété de tableau (état) à l’implémentation du canal 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 plus loin, nous n’aurons pas besoin de la propriété array dans l’implémentation du tube ni d’une solution de contournement pour la détection de la double modification.

Détection de changement de composant

Par défaut, sur chaque événement de navigateur, la détection de Angular passe en revue chaque composant pour voir s'il a été modifié - les entrées et les modèles (et peut-être d'autres éléments?) Sont vérifiés.

Si nous savons qu'un composant dépend uniquement 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 stratégie de détection de changement onPushname__, 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 sont modifiées et que les événements de modèle se déclenchent. Et, apparemment, nous n'obtenons pas cette erreur Expression ... has changed after it was checked avec ce paramètre. En effet, un composant onPushn'est pas vérifié à nouveau jusqu'à ce qu'il soit "marqué" (ChangeDetectorRef.markForCheck()) à nouveau. 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 pipes sans état/pures ne sont toujours pas exécutées à moins que leurs entrées changent. Donc, nous avons toujours besoin d'un tuyau stateful ici.

Voici la solution suggérée par @EricMartinez: canal avec état avec la détection de onPushchange. Voir la réponse de @ caffinatedmonkey pour cette solution.

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


Donc, après tout cela, j’aime bien combiner les réponses de @ Eric et de @ pixelbits: un canal avec état qui renvoie la même référence de tableau, avec la détection de onPushsi le composant le permet. Etant donné que le canal avec état renvoie la même référence de tableau, il peut toujours être utilisé avec des composants qui ne sont pas configurés avec onPushname__.

Plunker

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

137
Mark Rajcok

Comme Eric Martinez l'a souligné dans les commentaires, l'ajout de pure: false à votre décorateur Pipe et de changeDetection: ChangeDetectionStrategy.OnPush à votre décorateur Component corrigera votre problème. Voici un plunkr de travail. Changer en ChangeDetectionStrategy.Always, fonctionne aussi. Voici pourquoi.

Selon le guide angular2 sur les tuyaux :

Les pipes sont sans état par défaut. Nous devons déclarer un canal comme étant à état en définissant la propriété pure du décorateur @Pipe sur false. Ce paramètre indique au système de détection de changement d’Anngular de vérifier la sortie de ce tuyau à chaque cycle, que son entrée ait été modifiée ou non.

Pour ce qui est de ChangeDetectionStrategy , par défaut, toutes les liaisons sont vérifiées à chaque cycle. Lorsqu'un canal pure: false est ajouté, je pense que la méthode de détection de changement passe de CheckAlways à CheckOnce pour des raisons de performances. Avec OnPush, les liaisons du composant ne sont vérifiées que lorsqu'une propriété d'entrée est modifiée 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:

25
0xcaff

Demo Plunkr

Vous n'avez pas besoin de changer ChangeDetectionStrategy. L'implémentation d'un pipe stateful suffit à tout faire fonctionner.

C'est un tuyau avec état (aucun autre changement n'a été apporté):

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

De la documentation angulaire

Tuyaux purs et impurs

Il existe deux catégories de pipes: pure et impure. Les pipes sont pures par défaut. Chaque pipe que vous avez vue jusqu'à présent a été pure. Vous créez 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 un tuyau pur.

Tuyaux purs Angular n’exécute un tuyau pur que s’il détecte une modification pure de la valeur en entrée. Un changement pur est soit un changement d'une valeur d'entrée primitive (String, Number, Boolean, Symbol) ou une référence d'objet modifiée (Date, Array, Function, Object).

Angular ignore les modifications apportées aux objets (composites). Elle n'appellera pas un canal pur si vous modifiez un mois d'entrée, ajoutez-le à un tableau d'entrée ou mettez à jour une propriété d'objet d'entrée.

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

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

8
Sumit Jaiswal

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

laisser un article | pipe: param

0
Nick Bistrov

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

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

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

Une 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, vous n'avez même pas besoin d'un tuyau

0
Richard Luo

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 6 angulaire et devrait fonctionner pour d'autres versions angulaires.

0
ideeps