Filtrer une liste via un Pipe Angular
Tags: angular, typescript, tutorial
Published: 2022-02-05 08:33:24

👀 [Démo sur Stackblitz](https://stackblitz.com/edit/filtrer-une-liste-avec-rxjs-mseohp)

_Après avoir vu [comment filtrer une liste avec RXJS](https://dev.to/gfab/filtrer-une-liste-avec-rxjs-angular-44i2), j'ai pensé qu'il serait intéressant de voir comment on pourrait arriver au même résultat en étant un peu plus Angular Friendly._

Le Pipe Angular est parfait pour transformer une donnée depuis le template. Le principe est simple, on lui passe une valeur et ses arguments en entrée, sur laquelle on applique une transformation.

C'est exactement ce dont on a besoin !

---

### Les arguments

``` typescript
import { Pipe, PipeTransform } from '@angular/core';

type TList = string | number | IListItem;

interface IListItem {
  [key: string]: TList;
}

@Pipe({
  name: 'filter',
})
export class FilterPipe implements PipeTransform {
  public transform(
    list: Array<TList>,
    text: string,
    filterOn?: string
  ): TList[] {}
}
```

Un Pipe Angular implémente `PipeTransform` qui impose la méthode `transform`. Le premier argument (qui se situe sur la gauche du pipe) correspond à la valeur sur laquelle on applique la transformation. Puis suivent les arguments qui nous seront utiles pour notre filtrage.

Dans notre cas, nous nous attendons à recevoir une liste `list`, la recherche `text` que rentrera l'utilisateur et une clé `filterOn` sur laquelle filtrer, qui est optionnelle. Le tableau pourrait ne pas être un objet, mais une liste simple.

Nous connaissons plus ou moins la valeur de retour, c'est pour cela que j'ai défini une interface `IListItem` qui prend un type pour définir la valeur de chaque propriété `TList`, un type représentant soit un `number`, soit une `string` soit `IListItem` lui-même. Enfin notre valeur de retour qui sera du même type que `TList`.

---

🤜 _TypeScript est un outil génial, il fait partie intégrante d'Angular et pour le meilleur. Un bon typage du code permet d'éviter beaucoup d'erreurs, permet de mieux comprendre les contextes de fonctionnalités, facilite sa maintenance et son évolution._

---

### Le cas où le texte serait vide
 
``` typescript
public transform(
  list: Array<TList>,
  text: string,
  filterOn?: string
): TList[] {
  if (text === '') return list;
}
```

Le premier point à prendre en compte, également le plus simple à gérer, est que faire quand le texte est vide ? Simplement retourner le tableau d'entrée. Chaque fois que `text` sera vide on affichera le tableau initial.

### Quand les éléments de la liste ne sont pas des objets

``` typescript
public transform(
  list: Array<TList>,
  text: string,
  filterOn?: string
): TList[] {
  if (text === '') return list;

  return list.filter((item: TList) => {
    let valueToCheck: string = filterOn
      ? selectValue<TList>(item, filterOn)
      : `${item}`;

    if (valueToCheck) {
      valueToCheck = replaceDiacritics(valueToCheck)?.toLowerCase();
    }

    const formattedText: string = replaceDiacritics(text).toLowerCase();

    return valueToCheck?.includes(formattedText);
  });
}
```

J'utilise l'opérateur `filter` il nous retournera uniquement les valeurs du tableau qui respectent la condition.

Premièrement on vérifie si la propriété `filterOn` est définie, dans le cas où le troisième argument de notre `Pipe` serait défini, on suppose que notre liste est une liste d'objets.

Une fois la valeur trouvée, on la transforme en minuscule, ainsi, peu importe la casse, l'entrée est retrouvable.

Pour filtrer notre liste j'utilise `includes`.

On notera également l'utilisation de `toLowerCase()` sur l'argument `text` afin de conserver une cohérence avec la valeur trouvée dans l'objet. Ainsi peu importe la casse on saura retrouver les occurrences.

---

🤜 _J'utilise le point d'interrogation (?) pour prévenir des erreurs dans le cas où `valueToCheck` serait `null` ou `undefined`._

---

## Les diacritiques

Notre liste est maintenant correctement filtrée… Oui… mais [Thomas Sabre](https://www.linkedin.com/in/tsabre/) m'a fait remarqué que les caractères spéciaux ne sont pas pris en compte. Effectivement si notre valeur est "_J'ai mangé_" et que l'utilisateur entre "_j'ai mange_" notre pipe ne retournera aucun résultat.

### Alors comment gérer le cas des diacritiques ?

A chaque caractère lui est assigné un code, par exemple _A_ vaut **U+0041** quand _Z_ vaut **U+005A**. Les lettres sont différentes, les codes sont donc différents, facile et logique.

Bien… il en va de même pour les lettres accentuées. Quand pour l'humain il comprend que "_j'ai mange_" puisse faire référence à "_j'ai mangé_", nos machines, elles, nécessitent plus de précisions. En effet "_e_" et "_é_" sont différents. Tout comme "_é_" et "_è_" le sont aussi :

- e = U+0065
- é = U+00E9
- è = U+00E8

On comprend alors pourquoi notre pipe ne retrouve aucune valeur correspondante à "_J'ai mange_".

_é_ et _è_ sont basés sur _e_, grâce à cette base commune nous sommes capable de trouver une compatibilité entre ces caractères. [JavaScript nous offre la possibilité de normaliser facilement notre texte et de remplacer les occurences](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes) : 

`return value.normalize("NFD").replace(/\p{Diacritic}/gu, "")`

**NFD** (Normalization Form Canonical Decomposition) permet de décomposer les caractères, exemple :  **é = e +** ◌̀

Le `replace` recherche, quant à lui, toutes les occurrences diacritiques. Le flag `u` permet de supporter les caractères **Unicode** et le `g` les recherches dans toute la chaîne de caractères.

``` typescript
function replaceDiacritics(value: string): string {
  return value.normalize('NFD').replace(/\p{Diacritic}/gu, '');
}
```

## Les extras

### Filtrer dans un objet à plusieurs niveaux

Ok, c'est bien, mais dans un projet réel, parfois, souvent, la propriété sur laquelle on veut filtrer ne se trouve pas à la racine de l'objet. Alors comment faire, comment filtrer sur ces propriétés ?

``` typescript
<book-item *ngFor="let book of books | filter:author:'address.city'; trackBy: trackBySku" [book]="book"></book-item>
```

J'utilise un point pour indiquer que l'on souhaite accéder à une propriété, plus bas dans l'arborescence de l'objet. Chaque point serait un nœud.

``` typescript
function selectValue<TItem>(item: TItem, selector: string): string {
  if (!item) return;

  let value = null;

  if (selector.includes('.')) {
    value = selector
      .split('.')
      .reduce((previous: string, current: string) => previous[current], item);
  }

  return value ?? item[selector];
}
```

Dans un premier temps, je vérifie si `item` existe, s'il n'existe pas je ne vais pas plus loin dans la fonction. S'il existe, je vérifie si le sélecteur passé en paramètre a un point. Si c'est le cas, je split le sélecteur on aura `['address', 'city']`, sur lesquels on bouclera.

Grâce à `.reduce` on va pouvoir descendre jusqu'à la propriété demandée et retourner sa valeur.

Dans le cas où le sélecteur ne comporte pas de point (**.**) Cela signifie que la valeur se trouve à la racine de l'item de la liste passée en paramètre.

### Utiliser le pipe dans une classe

Je suis un grand adepte de **TypeScript**, un code bien décrit est un atout considérable, lors de la phase de développement et de debug.

``` typescript
public transform(
 list: Array<TList>,
 text: string,
 filterOn?: string
): TList[] {
   ...
}
```

Si je veux utiliser mon pipe dans un fichier **.ts**, je vais être confronté à des erreurs de typage, qu'on pourrait régler en mettant des `any` partout (non, ne faites pas ça 😢). Plus sainement, en une ligne on peut régler le problème tout en gardant une description propre de notre code :

``` typescript
public transform<T>(list: Array<T>, text: string, filterOn: string): Array<T>;
```

Et voilà, c'est propre, simple et on garde notre typage. Lorsqu'on utilisera notre pipe on sera en mesure de garder un typage fort et de travailler tout en profitant des avantages de TypeScript.

---

🤜 _Typescript offre la possibilité de typer de manière dynamique en utilisant des alias. L'alias va créer un nouveau nom qui fait référence au type qui lui est passé._

---

### Filtrer depuis plusieurs champs

``` typescript
<search-input (onSearch)="searchTerm = $event" placeholder="Title"></search-input>
<search-input (onSearch)="addressTerm = $event" placeholder="Address"></search-input>
<search-input (onSearch)="descriptionTerm = $event" placeholder="Sypnosis"></search-input>

<book-item *ngFor="let book of books
  | filter:searchTerm:'title'
  | filter:addressTerm:'address.city'
  | filter:descriptionTerm:'sypnosis'; trackBy: trackBySku"
  [book]="book"></book-item>
```

Filtrer une même liste suivant plusieurs critères (via plusieurs champs) peut être fait facilement. Il nous suffit de chaîner les pipes sur notre liste. Dans la limite du raisonnable, si vous avez une liste filtrable sur beaucoup de conditions, peut-être serait-il préférable de revoir le pipe.

---

👀 [Démo sur Stackblitz](https://stackblitz.com/edit/filtrer-une-liste-avec-rxjs-mseohp)

🤞 En lien
[Filtrer une liste avec RXJS et Angular](https://dev.to/gfab/filtrer-une-liste-avec-rxjs-angular-44i2)

❤ Merci à [Godson Yebadokpo](https://www.linkedin.com/in/godson-yebadokpo-3273035a/) pour la relecture.
❤ Merci à [Thomas Sabre](https://www.linkedin.com/in/tsabre/) pour son commentaire sur les dialectiques.

📸 [Photo by Joshua Rodriguez on Unsplash
](https://unsplash.com/@jcrod?utm_source=medium&utm_medium=referral)

Sur ce, bon dev ;-)