Appearance
Écouter les changements sur FormControl à l'aide d'un Observable
Dans cet article, nous allons voir comment détecter et réagir aux changements de valeur d'un champ de formulaire en utilisant les Observables dans Angular.
Comprendre le concept
Imaginez que vous êtes dans un café et que vous commandez un café. Vous ne voulez pas que le barista commence à préparer votre café à chaque fois que vous changez d'avis ("Un expresso... non, un cappuccino... ah non, finalement un latte..."). Vous préférez qu'il attende que vous ayez fini de réfléchir avant de commencer la préparation.
C'est exactement le même principe avec un champ de recherche : nous voulons éviter de déclencher une recherche à chaque frappe de clavier, mais plutôt attendre que l'utilisateur ait fini de taper avant d'effectuer la recherche.
Implémentation basique
Commençons par une implémentation simple pour comprendre le fonctionnement de base :
ts
import { Component, OnInit } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
@Component({
selector: 'app-search',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input type="text" [formControl]="searchField" placeholder="Rechercher un utilisateur...">
`
})
export class SearchComponent implements OnInit {
searchField = new FormControl('');
ngOnInit() {
this.searchField.valueChanges.subscribe(value => {
console.log('Nouvelle valeur:', value);
});
}
}
CONSEIL
L'utilisation de valueChanges
retourne un Observable qui émet une nouvelle valeur à chaque modification du champ.
Amélioration avec les opérateurs RxJS
Pour optimiser notre recherche, nous allons utiliser deux opérateurs RxJS très utiles :
debounceTime
: Attend un certain délai avant d'émettre la valeurdistinctUntilChanged
: Évite les émissions successives de valeurs identiques
Voici un exemple complet avec un service de recherche d'utilisateurs :
ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged, Subscription } from 'rxjs';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-search',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input type="text" [formControl]="searchField" placeholder="Rechercher un utilisateur...">
@if (users.length) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
}
`
})
export class SearchComponent implements OnInit, OnDestroy {
private userService = inject(UserService);
searchField = new FormControl('');
users: User[] = [];
private subscription!: Subscription;
ngOnInit() {
this.subscription = this.searchField.valueChanges.pipe(
debounceTime(400), // Attend 400ms après la dernière frappe
distinctUntilChanged() // Ignore si la valeur n'a pas changé
).subscribe(term => {
if (term) {
this.userService.searchUsers(term).subscribe(
users => this.users = users
);
} else {
this.users = [];
}
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
private http = inject(HttpClient);
searchUsers(term: string): Observable<User[]> {
return this.http.get<User[]>(`${this.apiUrl}?search=${term}`);
}
}
ts
export interface User {
id: number;
name: string;
username?: string;
email: string;
address?: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
}
};
phone?: string;
website?: string;
company?: {
name: string;
catchPhrase: string;
bs: string;
};
}
ATTENTION
N'oubliez pas de vous désabonner de l'Observable dans ngOnDestroy
pour éviter les fuites de mémoire.
Cas d'utilisation avancés
Voici quelques cas d'utilisation plus avancés avec d'autres opérateurs RxJS :
ts
this.searchField.valueChanges.pipe(
debounceTime(400),
distinctUntilChanged(),
filter(term => term.length >= 3), // Ne déclenche la recherche que si au moins 3 caractères
switchMap(term => this.userService.searchUsers(term)), // Passer à la prochaine requête HTTP
catchError(error => {
console.error('Erreur de recherche:', error);
return of([]); // Retourne un tableau vide en cas d'erreur
})
).subscribe(users => this.users = users);
BONNES PRATIQUES
- Utilisez toujours
debounceTime
pour les champs de recherche en temps réel - Pensez à
distinctUntilChanged
pour éviter les requêtes inutiles - Gérez correctement les erreurs avec
catchError
- Utilisez
switchMap
plutôt quemergeMap
pour les requêtes HTTP. Si l'observable est arrêté, la requête est annulée.
Conclusion
L'utilisation des Observables avec les FormControl permet de créer des interfaces utilisateur réactives et performantes. Les opérateurs RxJS comme debounceTime
et distinctUntilChanged
sont essentiels pour optimiser les performances et l'expérience utilisateur.
Références