Comment ajouter une validation personnalisée à un formulaire AngularJS?

278

J'ai un formulaire avec des champs de saisie et une configuration de validation en ajoutant les requiredattributs et autres. Mais pour certains domaines, je dois faire une validation supplémentaire. Comment puis-je «puiser» dans la validation qui FormControllercontrôle?

La validation personnalisée pourrait être quelque chose comme "si ces 3 champs sont remplis, alors ce champ est obligatoire et doit être formaté d'une manière particulière".

Il y a une méthode FormController.$setValiditymais qui ne ressemble pas à une API publique, je préfère donc ne pas l'utiliser. La création d'une directive personnalisée et son utilisation NgModelControllerressemblent à une autre option, mais exigeraient essentiellement que je crée une directive pour chaque règle de validation personnalisée, ce que je ne veux pas.

En fait, marquer un champ du contrôleur comme non valide (tout en restant FormControllersynchronisé) pourrait être la chose dont j'ai besoin dans le scénario le plus simple pour faire le travail, mais je ne sais pas comment faire.

botteaap
la source
4
Il y a un bel article sur le codage de monstre pour gérer les validations personnalisées dans JS angulaire. Vérifiez ce sur
Anshu
Ce n'est pas exactement ce que je recherche, car cela nécessite des directives personnalisées, mais j'accepterai votre réponse car c'est un bon article de toute façon.
botteaap
Je me demande la même chose, j'aimerais un certain contrôle au niveau du FormController. Par exemple, je veux que certaines directives personnalisées marquent l'instance FormController comme quelque chose comme formName.$warning.
Adam Waselnuk
2
Je crois que cela $$précède les apis non publics, avec le fait d' $être public. Voir stackoverflow.com/questions/19338493/…
Daniel F

Réponses:

370

Modifier: ajout d'informations ci-dessous sur ngMessages (> = 1.3.X).

Messages de validation de formulaire standard (1.0.X et supérieur)

Étant donné que c'est l'un des meilleurs résultats si vous effectuez une recherche sur "Angular Form Validation" sur Google, actuellement, je veux ajouter une autre réponse à toute personne venant de là.

Il y a une méthode dans FormController. $ SetValidity mais qui ne ressemble pas à une API publique, donc je préfère ne pas l'utiliser.

C'est "public", pas de soucis. Utilise le. C'est pour ça. S'il n'était pas destiné à être utilisé, les développeurs angulaires l'auraient privatisé lors d'une fermeture.

Pour effectuer une validation personnalisée, si vous ne souhaitez pas utiliser Angular-UI comme l'autre réponse suggérée, vous pouvez simplement lancer votre propre directive de validation.

app.directive('blacklist', function (){ 
   return {
      require: 'ngModel',
      link: function(scope, elem, attr, ngModel) {
          var blacklist = attr.blacklist.split(',');

          //For DOM -> model validation
          ngModel.$parsers.unshift(function(value) {
             var valid = blacklist.indexOf(value) === -1;
             ngModel.$setValidity('blacklist', valid);
             return valid ? value : undefined;
          });

          //For model -> DOM validation
          ngModel.$formatters.unshift(function(value) {
             ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
             return value;
          });
      }
   };
});

Et voici un exemple d'utilisation:

<form name="myForm" ng-submit="doSomething()">
   <input type="text" name="fruitName" ng-model="data.fruitName" blacklist="coconuts,bananas,pears" required/>
   <span ng-show="myForm.fruitName.$error.blacklist">
      The phrase "{{data.fruitName}}" is blacklisted</span>
   <span ng-show="myForm.fruitName.$error.required">required</span>
   <button type="submit" ng-disabled="myForm.$invalid">Submit</button>
</form>

Remarque: dans 1.2.X, il est probablement préférable de remplacer ng-if par - ng-showdessus

Voici un obligatoire lien plunker

En outre, j'ai écrit quelques entrées de blog sur ce sujet qui entrent dans un peu plus en détail:

Validation de forme angulaire

Directives de validation personnalisées

Edit: utilisation de ngMessages dans 1.3.X

Vous pouvez maintenant utiliser le module ngMessages au lieu de ngShow pour afficher vos messages d'erreur. Cela fonctionnera en fait avec n'importe quoi, ce ne doit pas être un message d'erreur, mais voici les bases:

  1. Comprendre <script src="angular-messages.js"></script>
  2. Référence ngMessagesdans votre déclaration de module:

    var app = angular.module('myApp', ['ngMessages']);
  3. Ajoutez le balisage approprié:

    <form name="personForm">
      <input type="email" name="email" ng-model="person.email" required/>
    
      <div ng-messages="personForm.email.$error">
        <div ng-message="required">required</div>
        <div ng-message="email">invalid email</div>
      </div>
    </form>

Dans le balisage ci-dessus, ng-message="personForm.email.$error"spécifie essentiellement un contexte pour les ng-messagedirectives enfants. Ensuite, ng-message="required"et ng-message="email"spécifiez les propriétés de ce contexte à surveiller. Plus important encore, ils spécifient également un ordre pour les enregistrer . Le premier qu'il trouve dans la liste qui est "véridique" l'emporte, et il montrera ce message et aucun des autres.

Et un plongeur pour l'exemple ngMessages

Ben Lesh
la source
6
Si vous retournez de la valeur sur la fonction que vous passez à $ parsers.unshift, des valeurs erronées seront également enregistrées dans le modèle - il serait préférable de renvoyer undefined je crois (lorsque la valeur n'est pas valide).
georgiosd
5
+1 @georgiosd ... 100% correct. En regardant ce que fait Angular, ils reviennent indéfinis. Ce n'est probablement pas un gros problème pour renvoyer la valeur, car (espérons-le) les modèles de formulaires invalides ne sont pas soumis ... mais mieux vaut prévenir que guérir, je suppose.
Ben Lesh
2
Super truc! Si vous êtes allé sur Google pour trouver un bon résumé de la validation personnalisée dans Angular, consultez ce que @blesh a écrit
maaachine
Avez-vous vérifié la validation de formulaire avancée avec AngularJS et les filtres ? Il résout la validation du filtre de manière générique.
Benny Bottema
1
Je pense que vous avez peut-être voulu faire return value ? valid : undefinedci - dessus.
GChorn
92

Le projet d'Angular-UI inclut une directive ui-validate, qui vous aidera probablement avec cela. Il vous permet de spécifier une fonction à appeler pour effectuer la validation.

Jetez un œil à la page de démonstration: http://angular-ui.github.com/ , recherchez la rubrique Valider.

Depuis la page de démonstration:

<input ng-model="email" ui-validate='{blacklist : notBlackListed}'>
<span ng-show='form.email.$error.blacklist'>This e-mail is black-listed!</span>

puis dans votre contrôleur:

function ValidateCtrl($scope) {
  $scope.blackList = ['[email protected]','[email protected]'];
  $scope.notBlackListed = function(value) {
    return $scope.blackList.indexOf(value) === -1;
  };
}
Pete BD
la source
Comme c'est étrange que cela ne fonctionne pas pour moi avec Angular 1.4
Nick
46

Vous pouvez utiliser ng-required pour votre scénario de validation ("si ces 3 champs sont remplis, alors ce champ est obligatoire":

<div ng-app>
    <input type="text" ng-model="field1" placeholder="Field1">
    <input type="text" ng-model="field2" placeholder="Field2">
    <input type="text" ng-model="field3" placeholder="Field3">
    <input type="text" ng-model="dependentField" placeholder="Custom validation"
        ng-required="field1 && field2 && field3">
</div>
Mario G.
la source
2
Cela a fonctionné pour moi. Pour les validations simples qui dépendent d'autres valeurs de champs, c'est la voie à suivre au lieu d'écrire des règles de validation complexes
VimalKumar
28

Vous pouvez utiliser Angular-Validator .

Exemple: utiliser une fonction pour valider un champ

<input  type = "text"
    name = "firstName"
    ng-model = "person.firstName"
    validator = "myCustomValidationFunction(form.firstName)">

Ensuite, dans votre contrôleur, vous auriez quelque chose comme

$scope.myCustomValidationFunction = function(firstName){ 
   if ( firstName === "John") {
       return true;
    }

Vous pouvez également faire quelque chose comme ceci:

<input  type = "text"
        name = "firstName"
        ng-model = "person.firstName"
        validator = "'!(field1 && field2 && field3)'"
        invalid-message = "'This field is required'">

(où champ1 champ2 et champ3 sont des variables d'étendue. Vous pouvez également vérifier si les champs ne correspondent pas à la chaîne vide)

Si le champ ne validatorréussit pas, le champ sera marqué comme invalide et l'utilisateur ne pourra pas soumettre le formulaire.

Pour plus d'exemples d'utilisation et d'exemples, voir: https://github.com/turinggroup/angular-validator

Avertissement: je suis l'auteur de Angular-Validator

user3920706
la source
13

J'ai récemment créé une directive pour permettre l'invalidation basée sur l'expression des entrées de forme angulaire. Toute expression angulaire valide peut être utilisée et prend en charge les clés de validation personnalisées à l'aide de la notation d'objet. Testé avec angulaire v1.3.8

        .directive('invalidIf', [function () {
        return {
            require: 'ngModel',
            link: function (scope, elm, attrs, ctrl) {

                var argsObject = scope.$eval(attrs.invalidIf);

                if (!angular.isObject(argsObject)) {
                    argsObject = { invalidIf: attrs.invalidIf };
                }

                for (var validationKey in argsObject) {
                    scope.$watch(argsObject[validationKey], function (newVal) {
                        ctrl.$setValidity(validationKey, !newVal);
                    });
                }
            }
        };
    }]);

Vous pouvez l'utiliser comme ceci:

<input ng-model="foo" invalid-if="{fooIsGreaterThanBar: 'foo > bar',
                                   fooEqualsSomeFuncResult: 'foo == someFuncResult()'}/>

Ou en passant simplement une expression (elle recevra la clé de validation par défaut "invalidIf")

<input ng-model="foo" invalid-if="foo > bar"/>
Alex Schwartz
la source
13

Voici une façon intéressante de faire des validations d'expressions génériques personnalisées dans un formulaire (à partir de: Validation de formulaire avancée avec AngularJS et filtres ):

<form novalidate="">  
   <input type="text" id="name" name="name" ng-model="newPerson.name"
      ensure-expression="(persons | filter:{name: newPerson.name}:true).length !== 1">
   <!-- or in your case:-->
   <input type="text" id="fruitName" name="fruitName" ng-model="data.fruitName"
      ensure-expression="(blacklist | filter:{fruitName: data.fruitName}:true).length !== 1">
</form>
app.directive('ensureExpression', ['$http', '$parse', function($http, $parse) {
    return {
        require: 'ngModel',
        link: function(scope, ele, attrs, ngModelController) {
            scope.$watch(attrs.ngModel, function(value) {
                var booleanResult = $parse(attrs.ensureExpression)(scope);
                ngModelController.$setValidity('expression', booleanResult);
            });
        }
    };
}]);

Démo jsFiddle (prend en charge la dénomination des expressions et plusieurs expressions)

C'est similaire à ui-validate, mais vous n'avez pas besoin d'une fonction de validation spécifique à la portée (cela fonctionne de manière générique) et bien sûr, vous n'avez pas besoin de ui.utils de cette façon.

Benny Bottema
la source
Merci. Très cool. Il est particulièrement utile d'appliquer des règles de validation pour les formulaires dynamiques. Cependant, il définit toujours la valeur du modèle même s'il n'est pas valide. Quoi qu'il en soit pour l'empêcher de définir le modelValue s'il n'est pas valide?
YuMei
5

Mettre à jour:

Version améliorée et simplifiée de la directive précédente (une au lieu de deux) avec la même fonctionnalité:

.directive('myTestExpression', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function (scope, element, attrs, ctrl) {
            var expr = attrs.myTestExpression;
            var watches = attrs.myTestExpressionWatch;

            ctrl.$validators.mytestexpression = function (modelValue, viewValue) {
                return expr == undefined || (angular.isString(expr) && expr.length < 1) || $parse(expr)(scope, { $model: modelValue, $view: viewValue }) === true;
            };

            if (angular.isString(watches)) {
                angular.forEach(watches.split(",").filter(function (n) { return !!n; }), function (n) {
                    scope.$watch(n, function () {
                        ctrl.$validate();
                    });
                });
            }
        }
    };
}])

Exemple d'utilisation:

<input ng-model="price1" 
       my-test-expression="$model > 0" 
       my-test-expression-watch="price2,someOtherWatchedPrice" />
<input ng-model="price2" 
       my-test-expression="$model > 10" 
       my-test-expression-watch="price1" 
       required />

Résultat: expressions de test mutuellement dépendantes où les validateurs sont exécutés lors du changement du modèle de directive de l'autre et du modèle actuel.

L'expression de test a une $modelvariable locale que vous devez utiliser pour la comparer à d'autres variables.

Précédemment:

J'ai tenté d'améliorer le code @Plantface en ajoutant une directive supplémentaire. Cette directive supplémentaire est très utile si notre expression doit être exécutée lorsque des modifications sont apportées à plusieurs variables ngModel.

.directive('ensureExpression', ['$parse', function($parse) {
    return {
        restrict: 'A',
        require: 'ngModel',
        controller: function () { },
        scope: true,
        link: function (scope, element, attrs, ngModelCtrl) {
            scope.validate = function () {
                var booleanResult = $parse(attrs.ensureExpression)(scope);
                ngModelCtrl.$setValidity('expression', booleanResult);
            };

            scope.$watch(attrs.ngModel, function(value) {
                scope.validate();
            });
        }
    };
}])

.directive('ensureWatch', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        require: 'ensureExpression',
        link: function (scope, element, attrs, ctrl) {
            angular.forEach(attrs.ensureWatch.split(",").filter(function (n) { return !!n; }), function (n) {
                scope.$watch(n, function () {
                    scope.validate();
                });
            });
        }
    };
}])

Exemple d'utilisation pour créer des champs à validation croisée:

<input name="price1"
       ng-model="price1" 
       ensure-expression="price1 > price2" 
       ensure-watch="price2" />
<input name="price2" 
       ng-model="price2" 
       ensure-expression="price2 > price3" 
       ensure-watch="price3" />
<input name="price3" 
       ng-model="price3" 
       ensure-expression="price3 > price1 && price3 > price2" 
       ensure-watch="price1,price2" />

ensure-expressionest exécuté pour valider le modèle lorsque l'une ng-modelou l'autre des ensure-watchvariables est modifiée.

knr
la source
4

@synergetic Je pense que @blesh suppose de mettre la fonction valider comme ci-dessous

function validate(value) {
    var valid = blacklist.indexOf(value) === -1;
    ngModel.$setValidity('blacklist', valid);
    return valid ? value : undefined;
}

ngModel.$formatters.unshift(validate);
ngModel.$parsers.unshift(validate);
Atul Chaudhary
la source
4

Validations personnalisées qui appellent un serveur

Utilisez l' API ngModelController$asyncValidators qui gère la validation asynchrone, comme faire une $httpdemande au backend. Les fonctions ajoutées à l'objet doivent renvoyer une promesse qui doit être résolue lorsqu'elle est valide ou rejetée lorsqu'elle n'est pas valide. Les validations asynchrones en cours sont stockées par entrée au clavier ngModelController.$pending. Pour plus d'informations, consultez AngularJS Developer Guide - Forms (Custom Validation) .

ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
  var value = modelValue || viewValue;

  // Lookup user by username
  return $http.get('/api/users/' + value).
     then(function resolved() {
       //username exists, this means validation fails
       return $q.reject('exists');
     }, function rejected() {
       //username does not exist, therefore this validation passes
       return true;
     });
};

Pour plus d'informations, voir


Utilisation de l' $validatorsAPI

La réponse acceptée utilise les pipelines $parserset $formatterspour ajouter un validateur synchrone personnalisé. AngularJS 1.3+ a ajouté une $validatorsAPI donc il n'est pas nécessaire de mettre des validateurs dans les pipelines $parserset $formatters:

app.directive('blacklist', function (){ 
   return {
      require: 'ngModel',
      link: function(scope, elem, attr, ngModel) {           
          ngModel.$validators.blacklist = function(modelValue, viewValue) {
              var blacklist = attr.blacklist.split(',');
              var value = modelValue || viewValue;
              var valid = blacklist.indexOf(value) === -1;
              return valid;
          });    
      }
   };
});

Pour plus d'informations, consultez AngularJS ngModelController API Reference - $ validators .

georgeawg
la source
3

Dans AngularJS, le meilleur endroit pour définir la validation personnalisée est la directive Cutsom. AngularJS fournit un module ngMessages.

ngMessages est une directive conçue pour afficher et masquer les messages en fonction de l'état d'un objet clé / valeur sur lequel il écoute. La directive elle-même complète les rapports de messages d'erreur avec l'objet d'erreur ngModel $ (qui stocke un état clé / valeur des erreurs de validation).

Pour la validation du formulaire personnalisé, il faut utiliser les modules ngMessages avec une directive personnalisée.Ici, j'ai une validation simple qui vérifiera si la longueur du numéro est inférieure à 6 afficher une erreur à l'écran

 <form name="myform" novalidate>
                <table>
                    <tr>
                        <td><input name='test' type='text' required  ng-model='test' custom-validation></td>
                        <td ng-messages="myform.test.$error"><span ng-message="invalidshrt">Too Short</span></td>
                    </tr>
                </table>
            </form>

Voici comment créer une directive de validation personnalisée

angular.module('myApp',['ngMessages']);
        angular.module('myApp',['ngMessages']).directive('customValidation',function(){
            return{
            restrict:'A',
            require: 'ngModel',
            link:function (scope, element, attr, ctrl) {// 4th argument contain model information 

            function validationError(value) // you can use any function and parameter name 
                {
                 if (value.length > 6) // if model length is greater then 6 it is valide state
                 {
                 ctrl.$setValidity('invalidshrt',true);
                 }
                 else
                 {
                 ctrl.$setValidity('invalidshrt',false) //if less then 6 is invalide
                 }

                 return value; //return to display  error 
                }
                ctrl.$parsers.push(validationError); //parsers change how view values will be saved in the model
            }
            };
        });

$setValidity est une fonction intégrée pour définir l'état du modèle sur valide / invalide

Muhammad Nasir
la source
1

J'ai étendu la réponse de @Ben Lesh avec une capacité à spécifier si la validation est sensible à la casse ou non (par défaut)

utilisation:

<input type="text" name="fruitName" ng-model="data.fruitName" blacklist="Coconuts,Bananas,Pears" caseSensitive="true" required/>

code:

angular.module('crm.directives', []).
directive('blacklist', [
    function () {
        return {
            restrict: 'A',
            require: 'ngModel',
            scope: {
                'blacklist': '=',
            },
            link: function ($scope, $elem, $attrs, modelCtrl) {

                var check = function (value) {
                    if (!$attrs.casesensitive) {
                        value = (value && value.toUpperCase) ? value.toUpperCase() : value;

                        $scope.blacklist = _.map($scope.blacklist, function (item) {
                            return (item.toUpperCase) ? item.toUpperCase() : item
                        })
                    }

                    return !_.isArray($scope.blacklist) || $scope.blacklist.indexOf(value) === -1;
                }

                //For DOM -> model validation
                modelCtrl.$parsers.unshift(function (value) {
                    var valid = check(value);
                    modelCtrl.$setValidity('blacklist', valid);

                    return value;
                });
                //For model -> DOM validation
                modelCtrl.$formatters.unshift(function (value) {
                    modelCtrl.$setValidity('blacklist', check(value));
                    return value;
                });
            }
        };
    }
]);
Liran Brimer
la source
0

Quelques grands exemples et bibliothèques présentés dans ce fil, mais ils n'avaient pas tout à fait ce que je cherchais. Mon approche: angular-valid - une bibliothèque de validation basée sur les promesses pour la validation asynchrone, avec un style Bootstrap optionnel intégré.

Une solution à validité angulaire pour le cas d'utilisation de l'OP pourrait ressembler à ceci:

<input  type="text" name="field4" ng-model="field4"
        validity="eval"
        validity-eval="!(field1 && field2 && field3 && !field4)"
        validity-message-eval="This field is required">

Voici un violon , si vous voulez le faire tourner. La lib est disponible sur GitHub , a une documentation détaillée et de nombreuses démos en direct.

2Toad
la source