lundi 14 octobre 2013

Validateur pour vérifier qu'une date est postérieure à une autre

Voici un exemple de validateur spécifique, qui répond au besoin classique de vérifier que la date saisie dans un champ est postérieure à celle saisie dans un autre champ du formulaire. Typiquement, on demande de saisir une date de début et une date de fin, et on veut s'assurer qu'un utilisateur étourdi n'a pas rentré une date de fin qui précède la date de début.

L'exemple est intéressant, car il permet de voir comment valider un champ par rapport à un autre, ce qui n'est pas expliqué dans la documentation d'AngularJS.



Pourquoi un validateur spécifique ?


Evidemment, on peut toujours conditionner l'affichage d'un message d'erreur sur une simple condition portant sur les propriétés du scope, par exemple :

endDate >= startDate

C'est tout simple, mais ça ne s'intègre pas à la validation des formulaires d'AngularJS. Le message d'erreur va s'afficher correctement, mais le formulaire ne sera pas considéré comme invalide.

Ça veut dire qu'il faut penser à ajouter aussi cette condition là où l'on se base sur la validité du formulaire, par exemple pour désactiver le bouton de soumission. Le fait d'ajouter une condition comme celle-ci de façon externe au mécanisme de validation du framework va conduire à devoir l'ajouter à plusieurs endroits, avec le risque d'oublier, et donc d'avoir des incohérences. Surtout si c'est un autre développeur qui ajoute plus tard une condition portant sur la validité du formulaire, il y a peu de chances pour qu'il pense à ajouter cette condition entre les deux dates.

C'est donc une solution simple, mais qui a un impact plutôt négatif sur la robustesse du code. Heureusement, ce n'est pas bien compliqué non plus de créer un vrai validateur qui s'intègre au mécanisme du framework.


Une directive pour valider


En réalité il n'y a pas en soi de validateurs dans AngularJS, ce qu'on crée pour ajouter des conditions de validation spécifiques, ce sont tout simplement des directives, qui peuvent ajouter ou non un parser au champ de saisie.

Donc ce qu'on va créer, c'est une directive dateAfter, à utiliser sur la date de fin, pour spécifier qu'elle doit être postérieure à la date de début :

<input ng-model="data.toDate" date-after="data.fromDate"/>

La valeur de cet attribut date-after placé sur la date de fin est une espression AngularJS, exactement la même que celle du ng-model du champ de saisie de la date de début.

Vous pouvez voir le code complet de l'exemple ici sur Plunker, et faire des essais. L'exemple utilise le date picker de UI Bootstrap pour la saisie de la date, lequel convertit la date saisie en un objet Date JavaScript dans le modèle, ce qui simplifie d'ailleurs la comparaison.

La directive elle-même n'est pas bien compliquée :

  .directive('dateAfter', function () {
    return {
      require: 'ngModel',
      link: function (scope, element, attrs, ngModelCtrl) {
        var date, otherDate;
        scope.$watch(attrs.dateAfter, function (value) {
          otherDate = value;
          validate();
        });
        scope.$watch(attrs.ngModel, function (value) {
          date = value;
          validate();
        });
        function validate() {
          ngModelCtrl.$setValidity('dateAfter',
                 !date || !otherDate || date >= otherDate);
        }        
      }
    };
  })

Elle récupère bien sûr le ngModelController (contrôleur créé par la directive ngModel) de l'élément courant, en l'occurrence le champ de saisie de la date de fin, pour pouvoir indiquer sa validité.

Or la validité doit être revérifiée si l'une ou l'autre des dates change. Il suffit pour cela de mettre deux watches sur le scope :

  • un qui porte sur l'autre date, en surveillant l'expression qui est dans l'attribut date-after (récupérée via attrs.dateAfter)
  • et l'autre qui porte sur la date du champ courant, le champ sur lequel est placée la directive, en surveillant l'expression de l'attribut ng-model (via attrs.ngModel)

Ces deux watches stockent la nouvelle valeur de l'expression qu'ils surveillent dans une variable locale de la directive, et déclenchent la fonction de validation. Dans la fonction de validation, il suffit d'utiliser la méthode $setValidity() du ngModelController pour positionner une clef de validation 'dateAfter' avec un booléen indiquant si le champ est valide.


Pas de parser ?


Les exemples de validateurs spécifiques qui sont dans la documentation d'AngularJS (Forms, § Custom Validation) ajoutent au ngModelControler un parser qui est chargé de valider la valeur saisie.

On pourrait faire la même chose pour la date du champ courant, mais dans ce cas il faudrait mettre le parser en fin de tableau, pour qu'il s'exécute après celui du date picker de UI Bootstrap.

Mais je préfère utiliser le $watch, car de cette façon, ça marche même si on modifie la date de début via du code JavaScript dans le contrôleur. Tandis qu'un parser refait la validation seulement quand l'utilisateur fait une saisie dans le champ, pas lorsque la propriété est modifiée par du code.

Voilà pour cet exemple qui pourra être utile à tous ceux qui ont besoin de valider un champ par rapport à un autre.


1 commentaire:

  1. Merci pour ce post :) Il est intéressant pour voir la façon de valider des champs dépendants les uns des autres.
    J'ai codé un petit test unitaire: https://gist.github.com/ojacquemart/7042210

    RépondreSupprimer