279

I have a form with input fields and validation setup by adding the required attributes and such. But for some fields I need to do some extra validation. How would I "tap in" to the validation that FormController controls?

Custom validation could be something like "if these 3 fields are filled in, then this field is required and needs to be formatted in a particular way".

There's a method in FormController.$setValidity but that doesn't look like a public API so I rather not use it. Creating a custom directive and using NgModelController looks like another option, but would basically require me to create a directive for each custom validation rule, which I do not want.

Actually, marking a field from the controller as invalid (while also keeping FormController in sync) might be the thing that I need in the simplest scenario to get the job done, but I don't know how to do that.

Rubens Mariuzzo
  • 25,735
  • 25
  • 111
  • 145
botteaap
  • 5,760
  • 3
  • 27
  • 35
  • 4
    There is a nice article on coding monster for handling custom validations in angular JS. Check [this](http://www.aleaiactaest.ch/custom-form-validation-in-angular-js/) out – Anshu Sep 25 '12 at 11:01
  • It's not exactly what I'm looking for, since it requires custom directives, but I'll accept your answer since it's a good article anyway. – botteaap Sep 26 '12 at 09:54
  • I'm wondering the same thing, I'd love some control at the level of the FormController. For example, I want certain custom directives to flag the FormController instance as something like `formName.$warning`. – Adam Waselnuk Nov 20 '14 at 14:45
  • 2
    I believe that `$$` precedes no-public apis, with `$` being public. See http://stackoverflow.com/questions/19338493/what-is-the-double-dollar-sign-used-for-in-angular – Daniel F Dec 25 '15 at 14:26

12 Answers12

372

Edit: added information about ngMessages (>= 1.3.X) below.

Standard form validation messages (1.0.X and above)

Since this is one of the top results if you Google "Angular Form Validation", currently, I want to add another answer to this for anyone coming in from there.

There's a method in FormController.$setValidity but that doesn't look like a public API so I rather not use it.

It's "public", no worries. Use it. That's what it's for. If it weren't meant to be used, the Angular devs would have privatized it in a closure.

To do custom validation, if you don't want to use Angular-UI as the other answer suggested, you can simply roll your own validation directive.

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;
          });
      }
   };
});

And here's some example usage:

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

Note: in 1.2.X it's probably preferrable to substitute ng-if for ng-show above

Here is an obligatory plunker link

Also, I've written a few blog entries about just this subject that goes into a little more detail:

Angular Form Validation

Custom Validation Directives

Edit: using ngMessages in 1.3.X

You can now use the ngMessages module instead of ngShow to show your error messages. It will actually work with anything, it doesn't have to be an error message, but here's the basics:

  1. Include <script src="angular-messages.js"></script>
  2. Reference ngMessages in your module declaration:

    var app = angular.module('myApp', ['ngMessages']);
    
  3. Add the appropriate markup:

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

In the above markup, ng-message="personForm.email.$error" basically specifies a context for the ng-message child directives. Then ng-message="required" and ng-message="email" specify properties on that context to watch. Most importantly, they also specify an order to check them in. The first one it finds in the list that is "truthy" wins, and it will show that message and none of the others.

And a plunker for the ngMessages example

Ben Lesh
  • 105,049
  • 47
  • 242
  • 231
  • 6
    If you return value on the function that you pass to $parsers.unshift, erroneous values will be saved to the model also - it'd be better to return undefined I believe (when the value is not valid). – georgiosd Mar 08 '13 at 13:24
  • 5
    +1 @georgiosd... 100% correct. Looking through what Angular does, they're returning undefined. It's probably not a *big* deal to return the value, as (hopefully) models from invalid forms aren't submitted... but better safe than sorry, I suppose. – Ben Lesh Mar 08 '13 at 14:55
  • 2
    Great stuff! If you Googled your way here looking for a good writeup on custom validation in Angular, check out what @blesh wrote – maaachine Jul 15 '13 at 14:31
  • Did you check [Advanced form validation with AngularJS and filters](http://blog.projectnibble.org/2014/01/05/advanced-form-validation-with-angularjs-and-filters/)? It solves the filter validation generically. – Benny Bottema Jan 05 '14 at 20:27
  • To make it work with the new ngMessages module the "ngModel.$parsers.unshift" should be "return value;" or it would show always the required message error instead of the correct message. – elecash May 21 '14 at 09:12
  • Not a bad answer but how does it answer the question: "if these 3 fields are filled in, then this field is required and needs to be formatted in a particular way". I need simple validation where either email or phone needs to be provided. If both are empty then it's "Please provide valid email or phone" is one of them is filled but invalid then it's "Please provide a valid email/phone". I was wondering how to access other inputs/values from the validate function as it's only passed the viewValue and control, not the form as a whole. – HMR Aug 14 '14 at 10:52
  • @HMR, If your form is set up correctly, you just have to do this: `Please provide email/phone` and `Please provide a valid email/phone` – Ben Lesh Aug 14 '14 at 16:47
  • @BenLesh Thank you for your reply, that won't work for me because it forces me to define the domain rules in the template(s). Have set up a general directive to be used as a proxy between the template and the rules service but not sure how to get the form: http://stackoverflow.com/q/25309522/1641941 – HMR Aug 15 '14 at 02:06
  • What if I have custom directive form element. It is not an input. Something like `
    ` And I want to use ngMessage to display error. I know how to create those errors but this. `myForm.email.$error`. How do I build path to that directive? what I place instead of email?
    – Sergey Romanov Jan 04 '15 at 15:30
  • @BenLesh are `ngMessages` still in beta? Seems, that they are now a full part of `AngularJS 1.3.x`. – Eugene Jan 16 '15 at 07:53
  • @Eugene no, they're no longer beta, they're in 1.3 which has been released. – Ben Lesh Jan 16 '15 at 16:38
  • "The first one it finds in the list that is "truthy" wins, and it will show that message and none of the others." – JustAMartin Jun 08 '15 at 08:02
  • Why is it "preferrable to substitute `ng-if` for `ng-show`" – Petah Jun 24 '15 at 21:45
  • @Petah really, it's case-by-case ng-if will teardown/setup directives under itself, and prevent leaving things wired up that don't need to be. But in simpler use cases like this, it probably doesn't matter that much, and it may even be faster to use ng-show, because you don't have to set up bindings and such again on whatever it's showing when it's revealed. – Ben Lesh Jun 24 '15 at 21:54
  • Why does the `$formaters` (i.e. DOM) section of the directive return `false` for invalid, while the $parsers` (i.e. model) section returns `undefined`? Why do they not both return `undefined`? – steampowered Jun 26 '15 at 18:20
  • @BenLesh could one through validator somehow say, that when some validator fails, then in the message connected to this validator use some variable. For example if validator checks for text length, then it is user friendly to tell, what is the expected length. Could one do that with `ngMessages`? – Eugene Sep 01 '15 at 09:22
  • Yes, you could do that @Eugene. It would only be tricky if the desired length was somehow dynamic. But even that I suspect wouldn't be too bad. – Ben Lesh Sep 01 '15 at 19:21
  • 1
    I think you may have meant to do `return value ? valid : undefined` above. – GChorn Sep 02 '15 at 21:49
  • @BenLesh Can you please share the validation for a checkbox set and not just a single checkbox value using ng-messages ? – Devner Jan 21 '16 at 06:39
  • Isn't this a little out of date? `$validators` are the suggested method in AngularJS 1.3 and on. https://busypeoples.github.io/post/forms-in-angularjs/ – elliottregan Aug 28 '17 at 19:05
93

Angular-UI's project includes a ui-validate directive, which will probably help you with this. It let's you specify a function to call to do the validation.

Have a look at the demo page: http://angular-ui.github.com/, search down to the Validate heading.

From the demo page:

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

then in your controller:

function ValidateCtrl($scope) {
  $scope.blackList = ['bad@domain.com','verybad@domain.com'];
  $scope.notBlackListed = function(value) {
    return $scope.blackList.indexOf(value) === -1;
  };
}
Pete BD
  • 10,031
  • 3
  • 29
  • 29
47

You can use ng-required for your validation scenario ("if these 3 fields are filled in, then this field is required":

<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.
  • 641
  • 1
  • 6
  • 10
  • 2
    This worked for me. For simple validations that depends on other fields values, this is the way to go instead of writting complex validations rules – VimalKumar Sep 07 '16 at 16:15
28

You can use Angular-Validator.

Example: using a function to validate a field

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

Then in your controller you would have something like

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

You can also do something like this:

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

(where field1 field2, and field3 are scope variables. You might also want to check if the fields do not equal the empty string)

If the field does not pass the validator then the field will be marked as invalid and the user will not be able to submit the form.

For more use cases and examples see: https://github.com/turinggroup/angular-validator

Disclaimer: I am the author of Angular-Validator

Mo.
  • 21,971
  • 31
  • 138
  • 201
user3920706
  • 457
  • 5
  • 3
13

Here's a cool way to do custom wildcard expression validations in a form (from: Advanced form validation with AngularJS and filters):

<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);
            });
        }
    };
}]);

jsFiddle demo (supports expression naming and multiple expressions)

It's similar to ui-validate, but you don't need a scope specific validation function (this works generically) and ofcourse you don't need ui.utils this way.

Benny Bottema
  • 9,605
  • 10
  • 59
  • 80
  • Thanks. Very cool. It is specially useful to apply validation rules for dynamic forms. However, it still set model value even it is invalid. Anyway to prevent it set the modelValue if it is invalid? – YuMei Jun 03 '15 at 16:03
13

I recently created a directive to allow for expression-based invalidation of angular form inputs. Any valid angular expression can be used, and it supports custom validation keys using object notation. Tested with angular 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);
                    });
                }
            }
        };
    }]);

You can use it like this:

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

Or by just passing in an expression (it will be given the default validationKey of "invalidIf")

<input ng-model="foo" invalid-if="foo > bar"/>
Alex Schwartz
  • 131
  • 1
  • 2
5

Update:

Improved and simplified version of previous directive (one instead of two) with same functionality:

.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();
                    });
                });
            }
        }
    };
}])

Example usage:

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

Result: Mutually dependent test expressions where validators are executed on change of other's directive model and current model.

Test expression has local $model variable which you should use to compare it to other variables.

Previously:

I've made an attempt to improve @Plantface code by adding extra directive. This extra directive very useful if our expression needs to be executed when changes are made in more than one ngModel variables.

.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();
                });
            });
        }
    };
}])

Example how to use it to make cross validated fields:

<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-expression is executed to validate model when ng-model or any of ensure-watch variables is changed.

knr
  • 765
  • 8
  • 7
4

@synergetic I think @blesh suppose to put function validate as below

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);
Nazik
  • 8,393
  • 26
  • 72
  • 115
Atul Chaudhary
  • 1,005
  • 1
  • 13
  • 15
4

Custom Validations that call a Server

Use the ngModelController $asyncValidators API which handles asynchronous validation, such as making an $http request to the backend. Functions added to the object must return a promise that must be resolved when valid or rejected when invalid. In-progress async validations are stored by key in ngModelController.$pending. For more information, see 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;
     });
};

For more information, see


Using the $validators API

The accepted answer uses the $parsers and $formatters pipelines to add a custom synchronous validator. AngularJS 1.3+ added a $validators API so there is no need to put validators in the $parsers and $formatters pipelines:

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;
          });    
      }
   };
});

For more information, see AngularJS ngModelController API Reference - $validators.

Community
  • 1
  • 1
georgeawg
  • 46,994
  • 13
  • 63
  • 85
3

In AngularJS the best place to define Custom Validation is Cutsom directive. AngularJS provide a ngMessages module.

ngMessages is a directive that is designed to show and hide messages based on the state of a key/value object that it listens on. The directive itself complements error message reporting with the ngModel $error object (which stores a key/value state of validation errors).

For custom form validation One should use ngMessages Modules with custom directive.Here i have a simple validation which will check if number length is less then 6 display an error on screen

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

Here is how to create custom validation directive

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 is inbuilt function to set model state to valid/invalid

Günter Zöchbauer
  • 490,478
  • 163
  • 1,733
  • 1,404
Muhammad Nasir
  • 1,882
  • 3
  • 33
  • 56
1

I extended @Ben Lesh's answer with an ability to specify whether the validation is case sensitive or not (default)

use:

<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
  • 2,992
  • 1
  • 24
  • 22
0

Some great examples and libs presented in this thread, but they didn't quite have what I was looking for. My approach: angular-validity -- a promise based validation lib for asynchronous validation, with optional Bootstrap styling baked-in.

An angular-validity solution for the OP's use case might look something like this:

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

Here's a Fiddle, if you want to take it for a spin. The lib is available on GitHub, has detailed documentation, and plenty of live demos.

2Toad
  • 13,711
  • 7
  • 38
  • 38