Accueil > Web > La validation de formulaires dans Angular

La validation de formulaires dans Angular

La validation de formulaire est une problématique récurrente dans la vie d’un développeur front-end. Dans les versions d’Angular inférieures à la 1.3, il n’y avait pas de mécanisme dédié à la validation, et la manière de procéder conseillée tenait alors plus du bricolage que d’une vraie solution.

Heureusement, depuis la v1.3, Angular intègre toute une pipeline de validation ; pipeline encore trop ignorée du grand public, alors même que la v1.2 est très loin derrière nous ! C’est ce à quoi nous allons tâcher de remédier dans cet article, en mettant en place une validation de formulaire sur un exemple concret.

Généralités

Pour commencer, mettons en place un formulaire très simple, par exemple pour créer un nouvel utilisateur :

http://jsfiddle.net/tz8ordr1/19/

Nous allons devoir effectuer quatre types de vérifications différentes :

  1. Vérifier que l’username, le password et la confirmation sont bien remplis ;
  2. Vérifier que l’username n’est pas déjà utilisé ;
  3. Vérifier que le password est valide (une majuscule, une minuscule et 6 caractères minimum) ;
  4. Vérifier que la confirmation est identique au password.

En Angular, les erreurs d’un formulaire sont contenues dans l’objet correspondant du formulaire. Par exemple, les erreurs sur le champ username sont contenues dans la variable de scope Main.registerForm.username.$error. Les erreurs sont associées à un libellé. On peut ainsi avoir comme contenu de cette variable de scope : { required: true }, indiquant qu’il y a une erreur sur la condition required.

L’attribut required des formulaires est d’ailleurs directement pris en charge par angular, de même que les attributs ng-minlength, ng-maxlength, ng-pattern, et divers types de champs (par exemple les champs de type email). Cela signifie donc que nous aurons uniquement besoin d’ajouter les attributs HTML correspondant, ce que nous allons faire immédiatement :

  • L’username est obligatoire :
<input type="text" id="username" name="username" ng-model="Main.user.username" 
  class="form-control" required />
  • Le password est obligatoire, doit faire 6 caractères au minimum et comporter au moins une majuscule et une minuscule :
<input type="password" id="password" name="password" ng-model="Main.user.password"
 class="form-control" required minlength="6" pattern="([A-Z].*[a-z]|[a-z].*[A-Z])" />
  • La confirmation est obligatoire :
<input type="password" id="confirmation" name="confirmation" 
 ng-model="Main.user.confirmation" class="form-control" required />

Essayez maintenant de valider le formulaire : http://jsfiddle.net/tz8ordr1/20/

Si vous avez un navigateur récent, vous devriez ne pas pouvoir valider le formulaire et apercevoir un message d’erreur du navigateur.

Pas très propre, n’est-ce pas ? Il faudrait pouvoir décider soi-même de comment afficher nos erreurs ! Et c’est ce que nous allons maintenant voir, grâce à l’utilisation de ngMessages.

Affichons nos erreurs avec ngMessages

Mise en place

Le module ngMessages permet d’afficher ou masquer des messages, chaque message étant contrôlé par une entrée clé / booléen dans un objet. Ca vous rappelle quelque chose ? C’est exactement le format de notre objet $error vu dans la première partie ! Voici comment on l’utilise :

<div ng-messages="Main.registerForm.username.$error"> 
    <ng-message when="required">This field is required</ng-message> 
</div>

De cette manière, dès que l’erreur required est levée, le message ci-dessus est affiché. Procédons de même avec les deux autres champs :

<div ng-messages="Main.registerForm.password.$error"> 
    <ng-message when="required">This field is required</ng-message> 
    <ng-message when="minlength">You need at least 6 characters</ng-message> 
    <ng-message when="pattern">You need at least an uppercase letter and a 
      lowercase one</ng-message> 
</div>
<div ng-messages="Main.registerForm.confirmation.$error"> 
    <ng-message when="required">This field is required</ng-message> 
</div>

Attention, n’oubliez pas d’inclure le module ngMessages, qui n’est pas inclus de base dans Angular ! Vous pouvez tester le rendu ici : http://jsfiddle.net/tz8ordr1/21/

Si vous essayez de valider le formulaire avec des erreurs, vous verrez que les messages de votre navigateur apparaissent toujours. Pour cela, il faut indiquer au navigateur de ne pas valider notre formulaire avec l’attribut novalidate, puis bloquer la validation du formulaire s’il est invalide grâce à  formController.$invalid :

http://jsfiddle.net/tz8ordr1/22/

On peut remarquer qu’il reste maintenant deux défauts principaux :

  1. Les erreurs apparaissent avant même que l’utilisateur ait saisi des données, ce qui est très perturbant ;
  2. On répète plusieurs fois la même ligne pour l’erreur de type required.

Voyons comment remédier à cela !

Utiliser des templates pour factoriser notre code

Il est possible de spécifier des messages d’erreur par défaut grâce à des templates avec ngMessages. Cela se fait de la façon suivante :

<script type="text/ng-template" id="default-errors"> 
    <ng-message when="required">This field is required</ng-message> 
</script>

// Puis plus loin par exemple :

<div ng-messages="Main.registerForm.username.$error"> 
    <ng-messages-include src="default-errors"></ng-messages-include>
</div>

Attention, cette syntaxe correspond à Angular 1.4+. Si vous êtes toujours en Angular 1.3, il faut utiliser un attribut :

<div ng-messages="Main.registerForm.username.$error" 
  ng-messages-include="default-errors"> 
</div>

Afficher les erreurs après saisie

Pour retarder l’affichage des erreurs, on peut utiliser deux propriétés de notre objet de formulaire : $dirty et $touched. La première vaudra false tant que le champ de formulaire n’aura pas été manipulé, puis true ensuite, alors que le second ne vaudra true qu’une fois le champs sélectionné puis quitté (= blur). A vous de décider suivant la situation, mais $touched est en général plus adapté à vos besoins.

Il faut cependant également afficher une erreur lorsque le formulaire a été soumis, même si les champs n’ont pas été manipulés. On utilisera pour cela l’attribut $submitted de notre formulaire. Voici donc un exemple de ce à quoi ressemble maintenant notre code :

<div ng-messages="Main.registerForm.password.$error" 
  ng-show="Main.registerForm.password.$touched || Main.registerForm.$submitted"> 
    <ng-messages-include src="default-errors"></ng-messages-include>
    <ng-message when="minlength">You need at least 6 characters</ng-message> 
    <ng-message when="pattern">You need at least an uppercase letter and a 
      lowercase one</ng-message> 
</div>

Code actuel : http://jsfiddle.net/tz8ordr1/28/

Des erreurs personnalisées

Il reste certaines erreurs que nous n’avons pas pu traiter avec les contraintes de validation déjà existantes dans Angular. Pour rappel :

  1. Vérifier que l’username n’est pas déjà pris ;
  2. Vérifier que la confirmation est identique au password.

Pour cela, nous allons créer nos propres directives qui effectueront ces vérifications. Pour cela, il faut comprendre comment Angular gère le modèle et la vue de nos champs de formulaire.

Il existe depuis longtemps dans Angular deux pipelines pour chaque champ de formulaire, l’une permettant de transformer une donnée du modèle pour l’afficher dans la vue ($formatters), et l’autre pour indiquer comment la valeur de la vue sera stockée dans le modèle ($parsers).

Ces deux pipelines (et plus particulièrement la pipeline $parsers) ont été empiriquement utilisées pour ajouter des contraintes de validation particulières, ce qui n’est pas leur rôle premier. Depuis la version 1.3, Angular intègre deux nouvelles pipelines, $validators et $asyncValidators qui sont elles dédiées à la validation. C’est ce que nous allons utiliser maintenant.

Confirmation identique au password

Nous allons donc créer une directive vEquals (j’utilise le préfixe v pour validation, libre à vous de choisir une autre norme) qui va prendre en argument la valeur que nous souhaitons que prenne notre champ de formulaire :

function vEqualsDirective($parse) {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
            ngModel.$validators.vEquals = function(value) {
                return value === $parse(attrs.vEquals)(scope);
            }
        }
    };
}
    
angular.module('app')
    .directive('vEquals', ['$parse', vEqualsDirective]);

Et les changements côté HTML

<div class="form-group">
 
 <label form="confirmation" class="form-label">Confirmation</label>
 
 <input type="password" id="confirmation" name="confirmation" 
   ng-model="Main.user.confirmation" class="form-control" required 
   v-equals="Main.registerForm.password.$viewValue" />
 
 <div ng-messages="Main.registerForm.confirmation.$error" 
  ng-show="Main.registerForm.confirmation.$touched || Main.registerForm.$submitted">
   <ng-messages-include src="default-errors"></ng-messages-include>
   <ng-message when="vEquals">The confirmation and the password are not 
    the same</ng-message>
 </div>
 
</div>

Comme vous pouvez le voir, je transmets à ma directive la $viewValue et non le modèle. En effet, si le champ password n’est pas valide, la valeur du modèle sera nulle, et notre validateur pourrait échouer alors que les deux sont identiques (ce qui est perturbant pour l’utilisateur). On utilise donc la $viewValue qui sera toujours égale au contenu du champ password.

Le code à l’état actuel : http://jsfiddle.net/tz8ordr1/30/

Vérifier l’unicité de l’username

Nous allons ici effectuer une validation asynchrone en utilisant la pipeline $asyncValidators. Angular s’attend à ce que notre fonction retourne une promise qui sera résolue par la suite. Ici, nous allons donc tester l’existence de notre utilisateur grâce à une API, puis rejeter la promise si on en trouve un.

function vUniqueUsernameDirective($http, $q) {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
        ngModel.$asyncValidators.vUniqueUsername = function(modelValue, viewValue) {
            return $http.get('/api/users/' + (modelValue || viewValue))
                .then(function resolved() {
                    return $q.reject();
                }, function rejected() {
                    return true;
                });
            }
        }
    };
}

angular.module('app')
    .directive('vUniqueUsername', ['$http', '$q', vUniqueUsernameDirective]);

<input type="text" id="username" name="username" ng-model="Main.user.username" 
  class="form-control" required v-unique-username />
<div ng-messages="Main.registerForm.username.$error" 
  ng-show="Main.registerForm.username.$touched || Main.registerForm.$submitted">
    <ng-messages-include src="default-errors"></ng-messages-include>
    <ng-message when="vUniqueUsername">This username is already used</ng-message>
</div>

Et voilà notre validation asynchrone mise en place !

Code actuel : http://jsfiddle.net/tz8ordr1/31/
(Attention, ce code ne fonctionnera pas, puisque la requête HTTP ne pointe vers aucun service.)

Améliorations

Une fois encore, on note quelques améliorations possibles :

  1. Si le contact de l’API est long, l’erreur peut s’afficher quelques temps après que l’utilisateur ait quitté le champ. S’il a scrollé plus bas, il pourrait ne pas la remarquer.
  2. Actuellement, à chaque caractère saisi, une requête est envoyée à notre API. Il faudrait pouvoir limiter le nombre de requêtes, voire ne les exécuter qu’au blur.

Pour le premier, rien de plus simple, il suffit d’afficher un message pendant la requête. Quand Angular résoud les validateurs asynchrones, il définit l’attribut $pending du champ à true :

<span ng-show="Main.registerForm.username.$pending">
    Checking the username...
</span>

Pour le second problème, nous allons considérer une fonctionnalité très utile d’Angular, les model-options et plus particulièrement l’option updateOn :

<input type="text" id="username" name="username" ng-model="Main.user.username" 
  class="form-control" required v-unique-username 
  ng-model-options="{ updateOn: 'blur' }" />

Vous pouvez également jeter un coup d’oeil sur les autres options proposées, dont l’option debounce, qu’on aurait également pu utiliser dans le cas présent.

Conclusion

Voilà notre système de validation totalement en place et respectueux à la fois de l’utilisateur (pas d’erreur affichée alors qu’il n’a encore rien fait) et des concepts d’Angular.

Le code final peut être trouvé ici : http://jsfiddle.net/tz8ordr1/32/

  1. 14/02/2016 à 01:59 | #1

    Merci, super utile et très complet !

  2. Abouelaiz
    16/02/2016 à 15:21 | #2

    Merci beaucoup , c’était très intéressant .

  3. 01/03/2016 à 09:34 | #3

    Ah oui, c’est vrai que c’est très loin du codage avec l’ancienne version. Donc, le formulaire ne tient pas compte d’une adresse email?

  4. Jordane Grenat
    03/03/2016 à 00:18 | #4

    @développement web
    Je n’ai pas très bien compris la question ?!

  1. Pas encore de trackbacks


7 × trois =