Retourner à : Angular et RxJS – Mini Introduction
Introduction
Lors de notre dernier chapitre dédié à RxJS nous avons regardé ce que sont les « Observables », « Observers » et « Subscriptions », aujourd’hui on va voir comment utiliser la méthode pipe pour manipuler les « Observables » à l’aide d’opérateurs et comment enchainer ces opérateurs.
Pour cela on va:
- expliquer ce qu’est la méthode « pipe »
- ensuite on verra le concepte d’opérateur
- et pour finir nous expliquerons les opérateurs « startWith », « map », « switchMap » et « debounceTime » à travers l’example d’une bare de recherche que nous allons implémenter ensemble.
La méthode pipe et les opérateurs

Commençons par expliquer ce qu’est la méthode « pipe » d’un « Observable ». La méthode pipe permet de prendre les événements émis par un « Observable » et d’y enchaîner plusieurs opérateurs les uns à la suite des autres. Les opérateurs quant à eux, sont des fonctions qui permettent de prendre les événements émis par un « Observable », de les manipuler, que ce soit pour les filtrer, les modifier, ou combiner les valeurs de plusieurs « Observables » entre elles et de retourner un nouvel « Observable » avec le résultat des ces opérations.
Il existe différents types d’operateurs, dont:
- des opérateurs de transformation, qui permettent de modifier les données émises par un observable
- des opérateurs de filtrage, qui comme le nom l’indique permettent de filtrer les données émises
- des opérateurs de gestion d’erreurs
- et des opérateurs utilitaires qui permettent entre autre de gérer les aspects de timing des évènements
Aujourd’hui on va voir des opérateurs de différents types afin de donner une idée général de ce qui est possible et dans les prochains chapitres on reviendra en détail sur chaque type d’observable.
Introduction de l’example:
Pour illustrer l’utilisation des opérateurs, j’ai préparé un petit exemple d’une bar de recherche. Laissez moi vous montrer en vitesse le code qu’on va utiliser. Tout d’abord j’ai créé une interface dans un fichier ‘models/person.model.ts’ et qui va représenter une personne, cette classe contient simplement trois champs:
export interface Person {
lastName: string;
firstName: string;
birthDate: Date;
}
Ensuite j’ai créé un service qui va simuler des appels API. Pour cela j’ai tout d’abord créé une liste avec plusieurs personnes, et une fonction “search” qui prend un text en paramètre et retourne un observable qui émet la liste des personnes dont les nom ou prénoms contiennent le texte recherché. Afin de mieux simuler un appel API je renvoie les résultats après un laps de temps aléatoire. Et le tout se trouve dans le fichier « services/person.service.ts »
import { Injectable } from '@angular/core';
import { Person } from '../models/person.model';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class PersonService {
private DATA: Person[] = [
{ lastName: 'Dupont', firstName: 'Jean', birthDate: new Date('1985-06-12')},
{ lastName: 'Durand', firstName: 'Marie', birthDate: new Date('1990-08-22')},
{ lastName: 'Martin', firstName: 'Paul', birthDate: new Date('1975-03-15')},
{ lastName: 'Bernard', firstName: 'Lucie', birthDate: new Date('2000-01-01')},
{ lastName: 'Leroy', firstName: 'Antoine', birthDate: new Date('1982-11-05')},
{ lastName: 'Moreau', firstName: 'Sophie', birthDate: new Date('1995-04-10')},
{ lastName: 'Petit', firstName: 'Alexandre', birthDate: new Date('1987-09-18')},
{ lastName: 'Roux', firstName: 'Camille', birthDate: new Date('1992-02-25')},
{ lastName: 'Fournier', firstName: 'Nicolas', birthDate: new Date('1989-12-03')},
{ lastName: 'Gauthier', firstName: 'Emma', birthDate: new Date('1996-07-14')},
{ lastName: 'Garcia', firstName: 'Lucas', birthDate: new Date('1998-05-23')},
{ lastName: 'Perrin', firstName: 'Chloé', birthDate: new Date('2001-03-11')},
{ lastName: 'Girard', firstName: 'Hugo', birthDate: new Date('1984-10-29')},
{ lastName: 'Bonnet', firstName: 'Manon', birthDate: new Date('1993-08-08')},
{ lastName: 'Masson', firstName: 'Julien', birthDate: new Date('1978-06-02')},
{ lastName: 'Faure', firstName: 'Laura', birthDate: new Date('1997-11-21')},
{ lastName: 'Riviere', firstName: 'Matthieu', birthDate: new Date('1983-05-30')},
{ lastName: 'Brun', firstName: 'Elodie', birthDate: new Date('2002-09-17')},
{ lastName: 'Blanc', firstName: 'Thomas', birthDate: new Date('1991-01-15')},
{ lastName: 'Henry', firstName: 'Alice', birthDate: new Date('1986-12-25')}
];
search(term: string): Observable<Person[]> {
const delay = Math.round(Math.random() * 400) + 100;
const filteredData = this.DATA.filter((item: Person) =>
item.firstName.toLowerCase().includes(term.toLowerCase()) ||
item.lastName.toLowerCase().includes(term.toLowerCase())
);
return new Observable(observer => {
setTimeout(() => {
observer.next(filteredData);
observer.complete();
}, delay);
});
}
}
Une fois le service prêt on a plus qu’à l’utiliser. Pour cela, j’ai créé un composant que j’ai appelé « SearchPageComponent », et dans le fichier ‘components/search-page/search-page.component.html », on se contente de créer
- un « input » dans lequel l’utilisateur pourra insérer la recherche à effectuer,
- un « div » ou on affichera le nombre de résultats retournés,
- et une liste avec le prénom, le nom et la date de naissance des personnes retournées par la recherche.
Pour l’instant on affiche du texte statique, mais nous verrons dans un instant comment remplacer cela par les données retournées par notre observable.
<div>
<input [formControl]="searchTextFormControl" placeholder="Rechercher un nom ou un prénom...">
<div>Nombre de résultats: 1</div>
<ul>
<li>John Doe - 19/08/1992</li>
</ul>
</div>
Définissons encore le « searchTextFormControl » dans le fichier « components/search-page/search-page.component.ts »:
import { Component } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-search-page',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent {
searchTextFormControl = new FormControl<string>('');
}
Il ne reste plus qu’à ajouter un peu de css pour rendre le tout plus agréable à l’oeil, pour cela on ajoute le css suivant au fichier ‘components/search-page/search-page.component.css »:
div {
max-width: 600px;
margin: 0 auto;
font-family: Arial, sans-serif;
}
input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
ul {
list-style-type: none;
padding: 0;
}
li {
width: 100%;
padding: 10px;
margin-bottom: 5px;
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
transition: background 0.3s ease;
}
li:hover {
background: #e0e0e0;
}
En plus de cela on va aussi utiliser un router avec l’url par défaut qui va pointer vers notre « SearchPageComponent ». Ajoutons donc la route suivant au fichier « app.route.ts »:
import { Routes } from '@angular/router';
import { SearchPageComponent } from './components/search-page/search-page.component';
export const routes: Routes = [{
path: '**',
component: SearchPageComponent
}];
Pour finir, n’oublions pas d’adapter le fichier « app.component.html » afin d’afficher les routes routes en question:
<strong><router-outlet /></strong>
Si nous lançons le programme et regardons le résultat dans le navigateur nous obtenons le résultat suivant:

Approche naïve sans utiliser les opérateurs RxJS
Maintenant que notre example est prêt, utilisons le service qu’on a créé à l’instant afin de récupérer la liste de personnes à afficher, et puis regardons comment utiliser des operateurs RxJS afin d’améliorer notre application.
L’approche la plus naïve à ce problème, serait de souscrire au changement de valeur du « searchTextFormControl », puis à chaque changement on pourrait faire une requête grâce à notre « PersonService » afin de récupérer les personnes correspondantes à notre recherche. Le tout bien entendu en faisant bien attention à garder une référence à chaque « Subscription » afin de pouvoir s’y désabonner lorsque notre composant est détruit. Une fois ces modifications faites, notre fichier « component/search-page/search-page.component.ts » ressemble à ceci:
import { Component, inject, OnDestroy } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { Subscription } from 'rxjs';
import { PersonService } from '../../services/person.service';
import { Person } from '../../models/person.model';
@Component({
selector: 'app-search-page',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent implements OnDestroy {
private personService = inject(PersonService);
searchTextFormControl = new FormControl<string>('');
subscriptions = new Subscription();
results: Person[] = [];
constructor() {
const searchTextChangeSubscription = this.searchTextFormControl.valueChanges.subscribe((value: string | null) => {
const searchText = value ? value : '';
const resultSubscription = this.personService.search(searchText).subscribe((result) => {
this.results = result;
});
this.subscriptions.add(resultSubscription);
});
this.subscriptions.add(searchTextChangeSubscription);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
}
Adaptons également le fichier html comme suit:
<div>
<input [formControl]="searchTextFormControl" placeholder="Rechercher un nom ou un prénom...">
<div>Nombre de résultats: {{ results.length }}</div>
<ul>
@for(person of results; track person) {
<li>
{{ person.firstName }} {{ person.lastName }} - {{ person.birthDate | date: 'shortDate':'':'fr-FR' }}
</li>
}
</ul>
</div>
Si nous regardons le résultat dans le navigateur, nous voyons qu’au début rien ne s’affiche, et dès qu’on commence à tapper une recherche les résultats de la recherche apparaissent dans notre liste. Nous verrons dans quelques instant pourquoi et comment résoudre cela.

En plus de cela on observe un second problème, si on modifie rapidement notre recherche, il se peut que le résultat affiché ne corresponde pas à la dernière recherche tapé, mais à un résultat précédent. Ceci est dû au délais que nous avons introduit dans notre Service, qui peut faire en sorte que les résultat n’arrivent pas dans l’ordre dans lequel les requêtes ont été lancés.
Les opérateurs
Repassons à notre code JavaScript, et commençons à y apporter quelques améliorations. Tout d’abord on y trouve un « subscribe » dans un « subscribe » qui ensuite insère le résultat de notre recherche dans une variable résultat.
Idéalement, on aurait préféré avoir un moyen de généré un nouvelle observable à partir de l’observable « valueChanges » de notre « FormControl ». Et bien ça tombe bien car RxJS vient avec un opérateur « switchMap » qui va pouvoir nous aider dans ce cas précis.
SwitchMap

« switchMap » permet de prendre les valeurs reçues d’un « Observable » et de retourner un nouvelle « Observable » à partir de ces valeurs. Le grand avantage de « switchMap » est que cet opérateur va se charger d’annuler tout subscribe à un « Observable » lancé précédemment et ne renverra que les résultat du dernier « Observable ». Concrètement si on regarde notre example, « switchMap » va s’abonner au « valueChanges » de notre « FormControl », à chaque frappe, le « switchMap » va créer et retourner un nouvel « Observable » qui n’est autre que notre requête de recherche, et si jamais on tape une nouvelle lettre avant que la recherche précédente n’ait abouti, « switchMap » ferra un unsubscribe à celle-ci et on n’aura donc que le résultat de la toute dernière recherche. Ceci résous donc notre problème de résultats qui pouvait arriver dans le mauvais ordre et ainsi fausser notre recherche.
Regardons maintenant comment traduire cela en code, pour cela on va utiliser la méthode pipe de notre « Observable » « valueChanges » du « FormControl », et dans ce « pipe », on aura en tant que paramètre l’opérateur qu’on souhaite utiliser, donc dans notre cas le « switchMap », et ce « switchMap » prend en paramètre le résultat émit par « valueChanges ». Nous pouvons donc utiliser ce résultat afin de créer notre « Observable » qui va effectuer la recherche et le retourner an tant que résultat de notre switchMap.
On n’a plus qu’a stoquer le tout dans une propriété de notre classe qu’on va appeler « results$ » qui sera du type « Observable<Person[]> » et le tour est joué. Notez que « switchMap » ainsi que les autres opérateurs que nous utiliserons ici, sont à importer de « rxjs »:
import { Component, inject } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { Observable, switchMap } from 'rxjs';
import { PersonService } from '../../services/person.service';
import { Person } from '../../models/person.model';
@Component({
selector: 'app-search-page',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent {
private personService = inject(PersonService);
searchTextFormControl = new FormControl<string>('');
results$: Observable<Person[]> = this.searchTextFormControl.valueChanges.pipe(
switchMap((searchText: string | null) => {
const search = searchText ? searchText : '';
return this.personService.search(search);
})
);
}
Nous pouvons maintenant adapter notre HTML afin d’utiliser ce nouvel « Observable ». Pour l’instant on va laisser la partie contenant le nombre de résultat de côté et on y reviendra dans un instant:
<div>
<input [formControl]="searchTextFormControl" placeholder="Rechercher un nom ou un prénom...">
<div>Nombre de résultats: -</div>
<ul>
@for(person of results$ | async; track person) {
<li>
{{ person.firstName }} {{ person.lastName }} - {{ person.birthDate | date: 'shortDate':'':'fr-FR' }}
</li>
}
</ul>
</div>
Si on retourne dans notre navigateur on voit qu’on a, à peu près le même résultat qu’avant, avec moins de code et à la différence près que nous avons réglé le problèmes des résultats qui pouvait potentiellement arriver dans le mauvais ordre.

StartWith
Passons maintenant à notre deuxième problème. « valueChanges » n’émet des valeurs que lorsque l’utilisateur change la valeur de l’input et donc lorsque nous démarrons l’application aucune valeur n’est émise et nous ne voyons donc aucun résultat à l’écran.

Ici l’opérateur startWith va pouvoir répondre à notre besoin. startWith prend un observable en input et retourne le même observable, à l’exception près que startWith va émettre en plus des valeurs émises par l’observable, une valeur initiale, donc ici on pourra émettre une chaine de caractère vide en tant que valeur initiale. pipe nous permettant d’enchainer les différents opérateurs, nous pourrons donc d’abord executer l’opérateur startWith afin d’émettre notre valeur initiale, suivi de l’opérateur switchMap que nous avons déjà implémenter. Pour echainer des opérateur les uns à la suite des autres, il suffit de les passer dans l’ordre souhaité en tant que paramètre à la méthode pipe, en les séparant par une virgule:
import { Component, inject } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { Observable, startWith, switchMap } from 'rxjs';
import { PersonService } from '../../services/person.service';
import { Person } from '../../models/person.model';
@Component({
selector: 'app-search-page',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent {
private personService = inject(PersonService);
searchTextFormControl = new FormControl<string>('');
results$: Observable<Person[]> = this.searchTextFormControl.valueChanges.pipe(
startWith(''),
switchMap((searchText: string | null) => {
const search = searchText ? searchText : '';
return this.personService.search(search);
})
);
}
Et maintenant si on ouvre notre navigateur on voit bien dès le début notre liste avec toutes les personnes qui s’affiche:

DebounceTime
Ce que nous avons là est déjà pas mal, mais ce code n’est pas très efficace car à chaque frappe dans notre input, un appel est fait à notre service. Afin de ne pas surcharger notre service, on pourrait attendre que l’utilisateur arrête de tapper sa requête avant de l’envoyer au service et d’afficher le résultat correspondant.

Pour cela nous pouvons prendre l’opérateur « debounceTime », qui prend en paramètre le temps d’inactivité en millisecondes requis avant d’émettre un évènement. Si un évènement est émis avant que le temps d’inactivité ce soit écoulé, l’évènement précédent n’est pas émis:
import { Component, inject } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { debounceTime, Observable, startWith, switchMap } from 'rxjs';
import { PersonService } from '../../services/person.service';
import { Person } from '../../models/person.model';
@Component({
selector: 'app-search-page',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent {
private personService = inject(PersonService);
searchTextFormControl = new FormControl<string>('');
results$: Observable<Person[]> = this.searchTextFormControl.valueChanges.pipe(
startWith(''),
debounceTime(500),
switchMap((searchText: string | null) => {
const search = searchText ? searchText : '';
return this.personService.search(search);
})
);
}
Et si on retourne dans le navigateur on constate bien que notre recherche n’est exécuté que si on arrête de tapper des caractères pendant au moins 500ms.

Map
Attaquons nous maintenant à notre “nombre de résultats”.

Pour calculer une valeur dérivé du résultat d’un autre « Observable » on peut utiliser l’opérateur « map », qui prend le résultat de l’ »Observable » en question en paramètre et retourne une nouvelle valeur calculée sur la base de la valeur émise.
Pour cela dans notre fichier TypeScript on va créer un nouvel « Observable » qu’on va appeler « numberOfResults$ », qui va être égal à l’ »Observable » « results$ » auquel on va appliquer l’opérateur « map » afin de retourner le nombre d’éléments présents dans le tableau émis par l’ »Observable » « results$ »:
import { Component, inject } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { debounceTime, map, Observable, startWith, switchMap } from 'rxjs';
import { PersonService } from '../../services/person.service';
import { Person } from '../../models/person.model';
@Component({
selector: 'app-search-page',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent {
private personService = inject(PersonService);
searchTextFormControl = new FormControl<string>('');
results$: Observable<Person[]> = this.searchTextFormControl.valueChanges.pipe(
startWith(''),
debounceTime(500),
switchMap((searchText: string | null) => {
const search = searchText ? searchText : '';
return this.personService.search(search);
})
);
numberOfResults$ = this.results$.pipe(
map(results => results.length),
);
}
Nous pouvons ensuite utiliser ce nouvel observable dans notre HTML:
<div>
<input [formControl]="searchTextFormControl" placeholder="Rechercher un nom ou un prénom...">
<div>Nombre de résultats: {{ numberOfResults$ | async }}</div>
<ul>
@for(person of results$ | async; track person) {
<li>{{ person.firstName }} {{ person.lastName }} - {{ person.birthDate | date: 'shortDate':'':'fr-FR' }}</li>
}
</ul>
</div>
Et voila nous avons maintenant un example fonctionnel qui affiche les résultats de notre recherche ainsi que le nombre de résultats retournés. Je tiens tout de même à mentionner que cet exemple est purement académique car on aurait pu récupérer le nombre d’éléments émis par notre « Observable » plus facilement et avec moins code en utilisant la syntaxe « @let » dans notre template. « @let » permet de définir une variable qu’on peut ensuite réutiliser dans notre template.
En utilisant « @let » on peut donc définir une variable dans notre template HTML qui va contenir les résultats émis par notre « Observable » « results$ », et ensuite en peut utiliser cette variable afin d’afficher ses résultats ainsi que le nombre de résultats retournés:
<div>
<input [formControl]="searchTextFormControl" placeholder="Rechercher un nom ou un prénom...">
@let persons = results$ | async;
<div>Nombre de résultats: {{ persons?.length }}</div>
<ul>
@for(person of persons; track person) {
<li>{{ person.firstName }} {{ person.lastName }} - {{ person.birthDate | date: 'shortDate':'':'fr-FR' }}</li>
}
</ul>
</div>
Et maintenant nous pouvons simplifier notre fichier TS en enlevant la partie « numberOfResults$ » comme ceci:
Maintenant vous devriez avoir compris comment fonctionne la méthode pipe ainsi que l’utilité des opérateurs RxJS. N’hésitez pas à me dire si il y a d’autres aspects de RxJS que vous aimeriez que je couvre dans cette mini introduction.