Dans des formulaires complexes, vous aurez souvent besoin d'implémenter vos propres stratégies de validation qui vont au-delà de ce que les Validators fournis par Angular vous permettent de faire.
Pour cela, il faudra créer vos propres Validators customisés !
Dans le cas de votre application, il faut être capable de comparer le contenu de deux champs, et de valider uniquement si les deux contiennent la même valeur.
Le Validator ne pourra donc pas être posé sur un FormControl comme vous en avez l'habitude : il devra être attribué au FormGroup.
Vous verrez donc comment :
créer votre propre Validator ;
attribuer un Validator à un FormGroup ;
afficher des messages d'erreur liés à vos Validators.
Vous allez commencer par créer un Validator extrêmement simple comme exercice : le FormControl sera valide uniquement s'il contient la string "VALID"
.
Créez un Validator simple
Commencez par créer un dossier validators
dans le dossier du module, et créez-y un fichier valid.validator.ts
.
Un Validator personnalisé, c'est une fonction qui retourne un ValidatorFn
. Un ValidatorFn
prend un AbstractControl
comme paramètre et retourne soit null
, soit un objet de type ValidationErrors
:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function validValidator(): ValidatorFn {
return (ctrl: AbstractControl): null | ValidationErrors => {
if (ctrl.value.includes('VALID')) {
return null;
} else {
return {
validValidator: ctrl.value
};
}
};
}
Le paramètre ctrl
est le FormGroup ou FormControl sur lequel ce Validator est placé. Dans ce Validator, vous regardez si la valeur du contrôle contient le texte 'VALID'
.
Si oui, le Validator retourne null
. Un Validator retourne null
lorsqu'il juge que le contrôle est valide.
Sinon, le Validator retourne un objet. La clé de l'objet est le nom que vous voulez associer à l'erreur (qui sera retrouvée via hasError
, par exemple). Comme valeur, je vous propose de passer la valeur du champ.
Pour le tester, je vous propose de l'ajouter au champ "Adresse mail" :
this.emailCtrl.addValidators([Validators.required, Validators.email, validValidator()]);
Du coup vous avez :
Vous pouvez ajouter la prise en charge de ce nouveau type d'erreur dans getFormControlErrorText
en ajoutant un cas :
else if (ctrl.hasError('validValidator')) {
return 'Ce texte ne contient pas le mot VALID';
}
Voilà ! Vous avez implémenté un Validator simple.
Maintenant, passons aux choses sérieuses : vous allez créer le fameux Validator qui comparera le contenu de deux champs.
Comparez deux champs
Dans votre dossier validators
, créez confirm-equal.validator.ts
.
Ce Validator devra être placé sur un FormGroup et non sur un FormControl.
Pourquoi ? Parce qu'il a besoin d'avoir accès à deux FormControls, enfants du FormGroup.
Du coup, l'implémentation que je vous propose est la suivante :
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function confirmEqualValidator(main: string, confirm: string): ValidatorFn {
return (ctrl: AbstractControl): null | ValidationErrors => {
if (!ctrl.get(main) || !ctrl.get(confirm)) {
return {
confirmEqual: 'Invalid control names'
};
}
const mainValue = ctrl.get(main)!.value;
const confirmValue = ctrl.get(confirm)!.value;
return mainValue === confirmValue ? null : {
confirmEqual: {
main: mainValue,
confirm: confirmValue
}
};
};
}
Le Validator requiert deux paramètres : les noms des deux champs à vérifier.
D'abord, vous vérifiez que les deux champs existent. Sinon, vous retournez une erreur.
Ensuite, si les deux champs contiennent des valeurs égales, vous retournez null
: sinon, vous retournez une erreur qui contient les deux valeurs comparées.
Le Validator est prêt ! Vous pouvez donc l'appliquer aux deux FormGroups emailForm
et loginInfoForm
, en passant un deuxième argument à FormBuilder.group
:
this.emailForm = this.formBuilder.group({
email: this.emailCtrl,
confirm: this.confirmEmailCtrl
}, {
validators: [confirmEqualValidator('email', 'confirm')]
});
// ...
this.loginInfoForm = this.formBuilder.group({
username: ['', Validators.required],
password: this.passwordCtrl,
confirmPassword: this.confirmPasswordCtrl
}, {
validators: [confirmEqualValidator('password', 'confirmPassword')]
});
Maintenant que le Validator est appliqué, il n'est plus possible de soumettre le formulaire si les valeurs contenues dans les deux champs ne sont pas égales – la validation est donc bien fonctionnelle !
Mais on a un petit problème : rien n'indique à l'utilisateur ce qui ne va pas !
En effet, un MatError ne s'affiche que lorsque le FormControl associé à son MatFormField contient une erreur. Du coup, lorsque c'est le FormGroup qui contient l'erreur, il nous faut une autre solution.
Affichez les messages d'erreur
Pour observer les changements d'état de validation des AbstractControls, il existe l'Observable statusChanges
.
Vous allez donc créer deux nouveaux Observables de type boolean
pour afficher et cacher vos messages d'erreur liés aux FormGroup.
Commencez par la correspondance des adresses mail :
this.showEmailError$ = this.emailForm.statusChanges.pipe(
map(status => status === 'INVALID'
);
Ajoutez un texte d'erreur à la MatCard, qui ne s'affiche que quand showEmailError$
émet true
:
<small class="error-text" *ngIf="showEmailError$ | async">Les deux adresses ne correspondent pas</small>
Petit hic : lorsqu'on teste, dès qu'on entre le moindre caractère...
Corriger ce bug se fera en deux étapes : d'abord, vous allez changer le rythme auquel la validation de ce FormGroup est évaluée. Par défaut, elle s'évalue à chaque changement. Vous allez faire en sorte qu'elle s'évalue uniquement lors de l'événementblur
d'un champ :
this.emailForm = this.formBuilder.group({
email: this.emailCtrl,
confirm: this.confirmEmailCtrl
}, {
validators: [confirmEqualValidator('email', 'confirm')],
updateOn: 'blur'
});
Ce qui donne :
C'est mieux, mais c'est toujours pas ça !
Au lieu d'évaluer seulement l'état de validation pour showEmailError$
, vous pouvez également vérifier que les deux champs contiennent une valeur :
this.showEmailError$ = this.emailForm.statusChanges.pipe(
map(status => status === 'INVALID' &&
this.emailCtrl.value &&
this.confirmEmailCtrl.value
)
);
Et ça y est ! Le message d'erreur ne s'affiche que lorsque l'utilisateur a entré deux adresses différentes, et changé de champ de formulaire.
Pour les mots de passe, c'est un peu plus fourbe, car si vous réutilisez la même logique :
this.showPasswordError$ = this.loginInfoForm.statusChanges.pipe(
map(status => status === 'INVALID' &&
this.passwordCtrl.value &&
this.confirmPasswordCtrl.value
)
);
<small class="error-text" *ngIf="showPasswordError$ | async">Les mots de passe ne correspondent pas</small>
Au début, ça a l'air de fonctionner.
Mais !
Si vous n'entrez rien en nom d'utilisateur, mais que vous entrez bien deux fois le même mot de passe :
Pourtant les mots de passe correspondent !
Donc qu'est-ce qui se passe ?
L'état de validation du FormGroup est bien à 'INVALID'
car le champ username
est requis ! Du coup, il faudra réagir uniquement à l'erreur visée :
this.showPasswordError$ = this.loginInfoForm.statusChanges.pipe(
map(status => status === 'INVALID' &&
this.passwordCtrl.value &&
this.confirmPasswordCtrl.value &&
this.loginInfoForm.hasError('confirmEqual')
)
);
Avec cette dernière étape, tous les messages d'erreur fonctionnent correctement, et votre formulaire complexe est terminé !
En résumé
Un Validator est une fonction qui retourne une
ValidatorFn
.Cette
ValidatorFn
prend un AbstractControl comme paramètre :elle retourne
null
si le contrôle est valide ;elle retourne un objet
ValidationErrors
si le contrôle est invalide – cet objet contient une paire clé-valeur où la clé est le nom de l'erreur.
Un Validator peut s'appliquer à un FormGroup en passant un objet de configuration comme deuxième argument à
FormBuilder.group
.Vous pouvez aussi passer
updateOn: 'blur'
à cet objet de configuration pour que la validation ne s'évalue que lors du blur d'un contrôle.Les AbstractControls ont un Observable
statusChanges
qui émet leur état de validation.
Bravo à vous ! Pour bien terminer cette partie du cours, je vous invite à faire le quiz. Après, nous nous attaquerons au state management dans la prochaine partie !