Cours Angular · Module 8: Formulaires
La validation - une fonction, un seul verdict
Dans cette leçon3
Le garde posté à la porte du dojo a une tâche simple: regarder celui qui se présente et rendre un verdict - il entre, ou il n'entre pas. Un validateur Angular est exactement cette fonction-là: il regarde la valeur d'un champ et renvoie l'un des deux résultats possibles - null quand tout va bien, ou un objet décrivant l'erreur quand quelque chose cloche. Retiens bien ce contrat (null = OK), car il revient à l'identique dans les trois parties de cette leçon.
Les validateurs intégrés
Les règles les plus courantes, Angular les a déjà prêtes - tu les accroches au champ sous forme de liste. Tu n'écris aucune logique, tu choisis simplement tes gardes sur l'étagère.
1import { Validators } from '@angular/forms';
2
3this.fb.group({
4 name: ['', Validators.required],
5 email: ['', [Validators.required, Validators.email]],
6 password: ['', [Validators.required, Validators.minLength(8)]],
7 age: [0, [Validators.min(18), Validators.max(120)]],
8 phone: ['', Validators.pattern(/^\d{9}$/)],
9 acceptTerms: [false, Validators.requiredTrue]
10});Observe le motif: quand il n'y a qu'une règle, tu la passes directement (Validators.required); quand il y en a plusieurs, tu les mets dans un tableau, et toutes doivent passer. Le champ email se lit donc ['', [Validators.required, Validators.email]] - la valeur initiale d'abord, le tableau des validateurs ensuite. À Validators.minLength(8) répond son miroir Validators.maxLength(50), quand tu veux aussi plafonner la longueur.
Et retiens le piège classique: Validators.required considère une case à cocher comme remplie qu'elle soit cochée ou non, alors que Validators.requiredTrue exige justement qu'elle soit cochée - c'est lui, le validateur de l'acceptation des conditions. Ne cherche pas plus loin: Validators.checked n'existe pas, et Validators.min(1) ne parle qu'aux nombres.
Les validateurs personnalisés
Quand la règle dont tu as besoin n'est pas sur l'étagère, tu écris ton garde toi-même. C'est une fonction qui respecte exactement le même contrat: elle reçoit le champ, elle renvoie null ou un objet d'erreurs.
1import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
2
3function forbiddenNameValidator(forbidden: string[]): ValidatorFn {
4 return (control: AbstractControl): ValidationErrors | null => {
5 if (forbidden.includes(control.value?.toLowerCase())) {
6 return { forbiddenName: { value: control.value } }; // invalide -> objet ValidationErrors
7 }
8 return null; // OK
9 };
10}
11
12// Utilisation
13this.fb.group({
14 name: ['', [Validators.required, forbiddenNameValidator(['admin', 'root'])]]
15});Arrête-toi sur la forme: la fonction extérieure reçoit la configuration (la liste des noms interdits) et renvoie le vrai validateur - c'est ce qui permet au même garde de servir plusieurs listes. Son type de retour, ValidatorFn, s'importe depuis @angular/forms au même titre que AbstractControl et ValidationErrors. La fonction intérieure, elle, garde toujours la même signature: (control: AbstractControl) => ValidationErrors | null - le contrôle entre, le verdict sort.
Le contrat est respecté à la lettre. Si la valeur figure parmi les noms interdits, la fonction renvoie un objet ValidationErrors, par exemple { forbiddenName: true } ou, comme ici, { forbiddenName: { value: control.value } } pour transporter le détail; sinon elle renvoie null. Rien d'autre ne fait l'affaire: pour dire que la valeur est valide, ce n'est ni true, ni undefined, ni un objet du genre { valid: true }, et pour signaler qu'elle est invalide, ce n'est ni false, ni un throw new Error() - un validateur ne lève jamais d'exception.
La clé de l'objet (forbiddenName) est le nom de l'erreur, celui que tu retrouveras plus tard dans le template - et c'est toi qui le choisis. Un validateur qui refuse les chiffres dans le nom du samouraï renverra plutôt { hasNumbers: true }, un validateur de robustesse de mot de passe { weakPassword: true }. Le squelette, lui, ne bouge pas: la même signature, le même null, le même objet d'erreurs.
Le validateur asynchrone
Certaines choses ne se vérifient pas sur place - savoir si un nom est déjà pris, seul le serveur le sait. Le validateur ne rend alors pas son verdict immédiatement: il renvoie un flux qui apportera le verdict.
1import { AsyncValidatorFn } from '@angular/forms';
2import { map, catchError, debounceTime, switchMap, first } from 'rxjs/operators';
3import { of } from 'rxjs';
4
5function uniqueNameValidator(service: SamuraiService): AsyncValidatorFn {
6 return (control: AbstractControl) => {
7 return of(control.value).pipe(
8 debounceTime(300), // ne pas interroger apres chaque lettre
9 switchMap(name => service.checkNameExists(name).pipe(
10 map(exists => exists ? { nameTaken: true } : null),
11 catchError(() => of(null)) // erreur reseau -> ne pas bloquer
12 )),
13 first()
14 );
15 };
16}
17
18// Les validateurs async sont le TROISIEME argument, separes des synchrones
19this.fb.group({
20 name: ['', [Validators.required], [uniqueNameValidator(this.service)]]
21});Le contrat est le même ({ nameTaken: true } ou null), simplement emballé dans un flux, parce que la réponse arrive en différé. Lis la chaîne .pipe() ligne par ligne, car c'est l'ordre qui fait tout: debounceTime(300) attend que l'utilisateur arrête de taper et épargne le serveur, switchMap(name => service.checkNameExists(name)) part alors interroger le service, map(exists => exists ? { nameTaken: true } : null) traduit en verdict le booléen reçu, catchError décide qu'une panne réseau ne doit pas bloquer le formulaire, et first() referme le flux dès le premier verdict rendu.
Retiens enfin l'endroit que l'on confond facilement: les validateurs asynchrones se passent en troisième argument de la définition du champ - la valeur initiale en premier, les validateurs synchrones en deuxième, les asynchrones en troisième. Ils ont bien leur place dans fb.group, Angular les attend simplement autrement.
Retiens de cette leçon un seul contrat: un validateur est une fonction qui renvoie null quand la valeur est correcte, ou un objet d'erreurs quand elle ne l'est pas. Les intégrés se prennent sur l'étagère, les tiens s'écrivent selon la même règle, et les asynchrones rendent leur verdict par un flux - tout le reste n'est que détail.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que doit renvoyer un validateur personnalisé quand la valeur est valide ?
2. Que doit renvoyer un validateur personnalisé quand la valeur est INVALIDE ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète le validateur personnalisé qui vérifie que le nom du samouraï ne contient pas de chiffres.
- Tri horizontal
Ordonne les éléments de la fonction validateur renvoyée (une seule ligne) : (control: AbstractControl) => ValidationErrors | null
- Tri vertical
Ordonne les opérateurs RxJS de la chaîne de validateur asynchrone qui vérifie l'unicité du nom (l'ordre à l'intérieur de .pipe(), ligne par ligne).