Filtrer une liste avec RXJS & Angular

gfab

Fabien

Posted on February 5, 2022

Filtrer une liste avec RXJS & Angular

👀 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;
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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

💖 đŸ’Ș 🙅 đŸš©
gfab
Fabien

Posted on February 5, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Filtrer une liste avec RXJS & Angular
angular Filtrer une liste avec RXJS & Angular

February 5, 2022