Les formulaires réactifs d'Angular offrent bien plus que la validation de surface. Dès qu'on sort des cas simples (email, required, minLength), on se heurte à des besoins métier spécifiques : un mot de passe doit contenir au moins une majuscule ET un chiffre, deux champs doivent correspondre, un identifiant ne peut pas être utilisé deux fois, ou le montant d'un remboursement ne peut pas dépasser le montant initial. C'est là qu'interviennent les validateurs personnalisés et la validation cross-field. Contrairement à ce qu'on peut croire, ce n'est pas compliqué—c'est juste une question de structure et de compréhension de la nature asynchrone de la validation.
Validateurs synchrones : la base solide
Un validateur synchrone en Angular est simplement une fonction qui reçoit un AbstractControl et retourne null (validation OK) ou un objet d'erreurs. Voici l'exemple basique : un validateur qui vérifie qu'un mot de passe contient au moins une majuscule et un chiffre. La fonction prend le contrôle en paramètre, accède à sa valeur, applique les règles métier, et retourne le résultat. Plutôt que d'utiliser des regex monstrueuses dans le template, on centralise la logique en TypeScript. C'est plus testable, plus maintenable, et on peut réutiliser le même validateur sur plusieurs formulaires. La clé est de retourner une clé d'erreur explicite—par exemple { passwordStrength: true } plutôt qu'un simple true—pour pouvoir afficher un message d'erreur précis dans le template.
function passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const hasUpperCase = /[A-Z]/.test(value);
const hasDigit = /\d/.test(value);
return hasUpperCase && hasDigit ? null : { passwordStrength: true };
}
// Utilisation
this.form = this.fb.group({
password: ['', [Validators.required, passwordStrengthValidator]]
});Validateurs asynchrones : quand la validation demande du temps
Les validateurs asynchrones deviennent nécessaires dès qu'on doit vérifier quelque chose contre un serveur ou une base de données : un email est-il déjà enregistré ? Un identifiant est-il disponible ? Un code de coupon est-il valide ? La différence clé : au lieu de retourner synchronement null ou un objet d'erreurs, on retourne une Observable ou une Promise. Angular attendra la réponse avant de marquer le contrôle comme valide ou invalide. Pendant ce temps, l'état du contrôle passe en pending, ce qui permet d'afficher un spinner ou un message "vérification en cours". C'est crucial pour l'UX : sans cela, l'utilisateur pense que son formulaire est valide alors qu'on est encore en train de vérifier.
function emailAvailabilityValidator(emailService: EmailService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) return of(null);
return emailService.checkEmail(control.value).pipe(
map(isAvailable => isAvailable ? null : { emailTaken: true }),
catchError(() => of(null)) // En cas d'erreur réseau, on laisse passer
);
};
}
// Utilisation
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email], [emailAvailabilityValidator(this.emailService)]]
});Validation cross-field : coordonner plusieurs champs
La validation cross-field est le moment où on compare ou coordonne plusieurs champs. Exemple classique : les champs "password" et "confirm password" doivent être identiques. Autre exemple : si l'utilisateur sélectionne "Autre" comme type de client, un champ texte "Précisez" devient obligatoire. Il y a deux approches : au niveau du FormControl (moins courant) ou au niveau du FormGroup. La seconde est préférable car elle donne accès à tous les champs du groupe. Au lieu de valider chaque champ isolément, on valide la cohérence de l'ensemble.
function passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return password && confirmPassword && password === confirmPassword ? null : { passwordMismatch: true };
}
this.form = this.fb.group(
{
password: ['', Validators.required],
confirmPassword: ['', Validators.required]
},
{ validators: passwordMatchValidator }
);Un exemple plus complexe : un formulaire de remboursement où le montant demandé ne peut pas dépasser le montant initial de la commande. On crée un validateur qui lit les deux champs, compare les valeurs, et retourne une erreur si la validation échoue. L'avantage de cette approche est qu'on centralise toute la logique métier au même endroit. Quand on change une valeur, Angular re-valide le groupe entier, ce qui assure la cohérence. On peut aussi appliquer des validateurs cross-field asynchrones : par exemple, vérifier auprès du serveur que cette combinaison de champs est valide selon les règles métier actuelles.
Pièges courants et solutions
Le premier piège : oublier que les validateurs asynchrones retournent une Observable. Si on essaie de traiter le résultat comme une valeur synchrone, on se retrouve avec des bugs silencieux. Solution : toujours utiliser les opérateurs RxJS appropriés (map, switchMap, etc.) et tester avec des marble tests si le projet est sérieux. Le second piège : appliquer un validateur asynchrone à chaque keystroke sans debounce. Cela crée des centaines de requêtes serveur inutiles. Solution : utiliser l'opérateur debounceTime dans le validateur lui-même ou, mieux, via updateOn: 'blur' dans la configuration du formulaire pour valider seulement au blur. Le troisième piège : mélanger la validation métier dans les templates avec du code TypeScript. Cela rend les templates illisibles et le code difficile à tester. Solution : centraliser toute la validation dans les validateurs et les services, les templates ne font que l'affichage.
Un piège spécifique aux validateurs cross-field : créer des dépendances circulaires ou des mises à jour en cascade qui figent le formulaire. Par exemple, si le validateur du champ A dépend du champ B et que le validateur du champ B dépend du champ A, on risque une boucle infinie. Solution : structurer les validateurs de façon unidirectionnelle, appliquer la validation au niveau du FormGroup plutôt qu'au niveau des champs individuels, et éviter de modifier les valeurs des autres champs depuis un validateur (utiliser des value changes listeners à la place).
Retour sur investissement : quand utiliser quoi
Un validateur personnalisé simple (ex: format de numéro de téléphone) prend 15 minutes à écrire une fois et économise 2 heures de débogage sur des validations mal placées en template. Un validateur asynchrone pour une vérification serveur prend une heure mais élimine complètement les faux positifs et améliore drastiquement l'UX. Un validateur cross-field pour une logique métier complexe peut sembler lourd au départ, mais c'est l'investissement qui garantit que votre formulaire reste cohérent même quand les exigences métier évoluent. Documentez vos validateurs personnalisés avec des exemples : le vous d'in six mois vous remerciera.
En production, les formulaires réactifs avec validateurs personnalisés et cross-field ne deviennent pas plus lents—ils deviennent plus fiables. La clé est de garder les validateurs simples, testables, et de respecter les patterns RxJS pour l'asynchrone. Quand un validateur devient trop complexe, c'est souvent un signal qu'il faut refactoriser la logique métier en amont. Et surtout, une validation côté client n'est jamais suffisante—dupliquez toujours vos règles de validation côté serveur.