đ DĂ©mo sur Stackblitz
Une fonctionnalitĂ© assez commune dans nos applications est le filtrage dâune liste en fonction des entrĂ©es de lâutilisateur. FonctionnalitĂ© qui peut ĂȘtre crĂ©Ă©e grĂące Ă RXJS.
Dans cet article, nous allons voir comment nous pourrions gĂ©rer le filtrage dâune liste au sein dâune application Angular et avec la librairie RXJS.
đ€ RXJS nous permet de contrĂŽler et de modifier un flux de donnĂ©es asynchrone.
L'exemple
Ajouter un champ simple qui permettrait de filtrer une liste de livres en fonction de la valeur entrĂ©e par lâutilisateur.
Comment faire ?
Pour ce faire nous allons décomposer notre fonctionnalité en plusieurs composants :
- Un composant qui sera en charge de lâaffichage des items de la liste :
BookItemComponent
; - Un composant pour le champ de recherche :
SearchInputComponent
; - Le composant principal :
BookListComponent
qui affichera le champ et la liste;
BookItemComponent
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
export interface IBook {
sku: string;
title: string;
sypnosis: string;
}
@Component({
selector: 'book-item',
template: `
<article class="card">
<h2>
{{ book.title }}
</h2>
<p>{{ book.sypnosis }}</p>
</article>
`,
styles: [
`
article {
border-radius: 2px;
display: inline-block;
width: 400px;
padding: 10px;
margin-top: 10px;
background-color: #fff;
border: 1px solid rgba(200, 200, 200, 0.75);
}
h2, p {
margin: 0;
}
h2 {
font-size: 1.2rem;
margin-bottom: 5px;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BookItemComponent {
@Input()
public book!: IBook;
}
Je débute par BookItemComponent
. Un simple composant dâaffichage qui correspond au contenu de chaque item du Array qui sera affichĂ©, on passera les donnĂ©es par item
.
đ€ On utilise ChangeDetectionStrategy.onPush
pour faire en sorte que le composant ne détecte les changements que si :
- Au moins lâune de ses valeurs dâentrĂ©e a changĂ©
- Un Ă©vĂ©nement provient du composant lui-mĂȘme ou de lâun de ses enfants
- On exécute la détection des changements de maniÚre explicite, avec
ChangeDetectorRef
, par exemple - Le pipe asynchrone (async) est utilisé dans le HTML
SearchInputComponent
import {
Component,
EventEmitter,
Output,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'search-input',
template: `
<input type="text" [formControl]="searchControl" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchInputComponent implements OnInit, OnDestroy {
@Output()
public onSearch: EventEmitter<string> = new EventEmitter<string>();
public searchControl: FormControl = new FormControl('');
private readonly searchSubscription: Subscription = new Subscription();
public ngOnInit(): void {
const searchInput$ = this.searchControl.valueChanges
.pipe(distinctUntilChanged(), debounceTime(300))
.subscribe((text: string) => {
this.onSearch.emit(text);
});
this.searchSubscription.add(searchInput$);
}
public ngOnDestroy(): void {
this.searchSubscription.unsubscribe();
}
}
Pour Ă©couter les changements dans le champ, jâai dĂ©cidĂ© dâutiliser ReactiveFormsModule
qui propose une API assez complĂšte pour gĂ©rer les formulaires. De cette API, ce qui mâintĂ©resse est valueChanges
qui retourne la derniĂšre valeur Ă chaque changement provenant, dans notre cas, du FomControl
: searchControl
.
Dans le pipe qui suit, Ă valueChanges
je lui passe deux opérateurs :
debounceTime(300)
: Prend en paramĂštre le temps dâattente avant la reprise du stream. Dans notre cas, 300ms, on attend donc 300ms avant de passer au prochain opĂ©rateur. Si dans les 300ms la valeur change de nouveau, le compteur se remet Ă 0.
distincUntilChanged
: Compare la valeur précédente et la valeur courante. Il fonctionne comme une condition, si la nouvelle valeur est différente de la valeur précédente alors il passe au prochain opérateur.
AprÚs une attente de 300ms et aprÚs avoir vérifié que la valeur courante est différente de la valeur précédente, elle est émise au composant parent.
đ€ Pourquoi unsubscribe ?
Pour des problÚmes de mémoire, de fuite de mémoire et pour contrÎler le flux de données afin d'éviter des effets secondaires. Dans certain cas il faut se désabonner explicitement, dans notre cas, à la destruction du composant dans lequel il se situe.
BookListComponent
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from '@angular/core';
import { map } from 'rxjs/operators';
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
import { IBook } from './book-item.component';
@Component({
selector: 'book-list',
template: `
<search-input (onSearch)="search($event)"></search-input>
<ng-container *ngIf="(books$ | async) as books">
<book-item *ngFor="let book of books; trackBy: trackBySku;" [book]="book"></book-item>
</ng-container>
`,
styles: [
`
book-item {
display: block;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BookListComponent implements OnInit {
@Input()
public books: IBook[] = [];
public books$!: Observable<IBook[]>;
public readonly trackBySku: (index: number, item: IBook) => string = (
index: number,
item: IBook
) => item.sku;
private readonly searchFilter: BehaviorSubject<string> = new BehaviorSubject(
''
);
private readonly searchText$: Observable<string> =
this.searchFilter.asObservable();
public ngOnInit(): void {
const listOfBooks$: Observable<IBook[]> = of(this.books);
this.books$ = combineLatest([listOfBooks$, this.searchText$]).pipe(
map(([list, search]: [IBook[], string]) =>
this.filterByName(list, search)
)
);
}
public search(value: string): void {
this.searchFilter.next(value);
}
private filterByName(list: IBook[], searchTerm: string): IBook[] {
if (searchTerm === '') return list;
return list.filter(
(item: IBook) =>
item.title.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
);
}
}
Analysons cette classe. Nous avons this.books$
qui est initialisé dans le ngOnInit
. Il rĂ©cupĂ©re les valeurs passĂ©es par lâentrĂ©e books, autrement dit la liste (un Array) et la valeur retournĂ©e par le searchFilter$
, correspondant au texte rentré dans le champ.
Ces deux variables sont passées en argument à combineLatest
qui, dans ce cas, est trĂšs utile, car lorsque lâun des observables Ă©met une valeur, il combine les valeurs les plus rĂ©centes de chaque source. Les donnĂ©es dâentrĂ©e (books) ne changent pas, câest la liste initiale, celle quâon voit affichĂ©e Ă lâinitialisation du composant. Quant Ă this.searchText$
, il change de valeur à chaque entrée dans le champ texte.
Suit la variable searchText$
qui récupÚre le flux du BehaviorSubject
. Celle-lĂ mĂȘme qui est utilisĂ©e dans le combineLatest
.
Voyons la fonction search(value: string)
, elle est appelée quand un nouvel événement est détecté, soit à chaque fois que le composant enfant SearchInputComponent
notifie le parent dâun changement dans le champ texte. search(value: string)
pousse dans le BehaviorSubject
la nouvelle valeur, cette nouvelle valeur passe par les opérateurs que nous venons de décrire.
Quand il y a un changement, les valeurs des deux observables Ă©coutĂ©s passent par lâopĂ©rateur map qui appelle la fonction filterByName(list: IBook[], searchTerm: string)
(dont list est et restera le tableau initial), fonction qui, si a searchTerm
Ă vide retourne toute la liste, sinon effectue le tri et retourne les noms correspondants Ă la recherche.
đ€ trackBy
permet Ă Angular de savoir si lâune des valeurs du tableau a changĂ©. Il sâagit dâune fonction qui dĂ©finit comment suivre les modifications apportĂ©es aux Ă©lĂ©ments dâun itĂ©rable.
A chaque fois que lâon ajoute, dĂ©place, modifie ou supprime des Ă©lĂ©ments dans le tableau, la directive va rechercher quel Ă©lĂ©ment de ce tableau il doit modifier pour uniquement mettre cet Ă©lĂ©ment Ă jour. Sans cette directive, lâitĂ©rable entier serait actualisĂ©.
Un gage de performance notamment sur des listes longues et/ou des listes qui sont vouées à subir beaucoup de modifications.
đ DĂ©mo sur Stackblitz
đ€ En lien
Filtrer une liste via un pipe Angular (BientĂŽt)
†Merci à Godson Yebadokpo pour la relecture.
đž Photo by Jacek on Unsplash
Sur ce, bon dev ;-)
Top comments (0)