Filtrer une liste avec RXJS & Angular
Tags: angular, french, tutorial
Published: 2022-02-05 08:13:30

👀 [Démo sur Stackblitz](https://stackblitz.com/edit/filtrer-une-liste-avec-rxjs-ljnh1n?file=src/app/app.component.ts)

_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**

``` typescript
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**

``` typescript
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**

``` typescript
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](https://stackblitz.com/edit/filtrer-une-liste-avec-rxjs-ljnh1n?file=src/app/app.component.ts)

🤞 **En lien**
[Filtrer une liste via un pipe Angular (Bientôt)](https://dev.to/gfab/filtrer-une-liste-via-un-pipe-angular-4kbp)

❤ Merci à [Godson Yebadokpo](https://www.linkedin.com/in/godson-yebadokpo-3273035a/) pour la relecture.

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

Sur ce, bon dev ;-)