• 12 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 20/06/2022

Créez votre propre Validator

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 :

Une erreur inconnue
Une erreur inconnue

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...

Trop tôt !
Trop tôt !

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 :

Toujours trop tôt !
Toujours trop tôt !

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 :

Quoi ?!
Quoi ?!

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 !

Exemple de certificat de réussite
Exemple de certificat de réussite