Après avoir vu comment filtrer une liste avec RXJS, 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
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
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
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 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 :
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.
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 ?
<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.
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.
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 :
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
<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.
🤞 En lien
Filtrer une liste avec RXJS et Angular
❤ Merci à Godson Yebadokpo pour la relecture.
❤ Merci à Thomas Sabre pour son commentaire sur les dialectiques.
📸 Photo by Joshua Rodriguez on Unsplash
Sur ce, bon dev ;-)
Top comments (0)