Question

I have a simple html form containing regular text input. ng-minlength, ng-maxlength and ng-pattern angular built-in form input directives are set on the input.

Problem: ng-pattern check is applied before the length check by ng-minlength and ng-maxlength.

Question: how can I change the default check order: i.e. first check for the length, then apply pattern check?

Example:

<body ng-app>

    <div>
        <form name="myForm">
            Name: <input name="name" type="text" ng-model="name" ng-minlength="3" ng-maxlength="16" ng-pattern="/^\w+$/"/>
            <div ng-show="myForm.name.$dirty && myForm.name.$invalid">
                <span ng-show="myForm.name.$error.pattern">Pattern error</span>
                <span ng-show="myForm.name.$error.minlength || myForm.name.$error.maxlength">Length error</span>
            </div>
            <br/>
            <input type="submit" value="Submit">
        </form>
    </div>

</body>

Current behavior:

  • enter "#" - see "Pattern error"
  • enter "###" - see "Pattern error"

Desired behavior:

  • enter "#" - see "Length error"
  • enter "###" - see "Pattern error"

FYI, related jsfiddle.

Thanks in advance.

Was it helpful?

Solution

Write your own directive:

var mod = angular.module("myApp", []);

mod.directive("nameValidation", function () {
    return {
        restrict: "A",
        require: "ngModel",
        link: function (scope, element, attrs, ngModelCtrl) {
            var validate = function (value) {
                var minLen = parseInt(attrs.myMinlength, 10),
                    maxLen = parseInt(attrs.myMaxlength, 10),
                    pattern = attrs.myPattern,
                    match = pattern.match(/^\/(.*)\/([gim]*)$/),
                    lenErr = false;

                if (match) {
                    pattern = new RegExp(match[1], match[2]);   
                }
                if (!ngModelCtrl.$isEmpty(value)) {
                    ngModelCtrl.$setValidity("pattern", true);
                    if ((minLen && value.length < minLen) || (maxLen && value.length > maxLen)) {
                        ngModelCtrl.$setValidity("length", false);
                        lenErr = true;
                    }
                    else {
                        ngModelCtrl.$setValidity("length", true);
                        lenErr = false;
                    }

                    if (!lenErr) {
                        if (match && !pattern.test(value)) {
                            ngModelCtrl.$setValidity("pattern", false);
                        }
                        else {
                            ngModelCtrl.$setValidity("pattern", true);
                        }
                    }
                }
                else {
                    ngModelCtrl.$setValidity("length", true);
                    ngModelCtrl.$setValidity("pattern", true);
                }
            }

            ngModelCtrl.$parsers.push(validate);
            ngModelCtrl.$formatters.push(validate);
        }
    }
});

Then in your HTML, include the app and use the directive:

<body ng-app="myApp">

<div>
    <form name="myForm">
        Name: <input name="name" type="text" ng-model="name" name-validation="" my-minlength="3" my-maxlength="16" my-pattern="/^\w+$/"/>
        <div ng-show="myForm.name.$dirty && myForm.name.$invalid">
            <span ng-show="myForm.name.$error.pattern">Pattern error</span>
            <span ng-show="myForm.name.$error.length">Length error</span>
        </div>
        <br/>
        <input type="submit" value="Submit">
    </form>
</div>
</body>

The directive uses my-minlength, my-maxlength, and my-pattern for the three values. If length fails, that will trip first. If not, then pattern will show as error if it doesn't match. Consider renaming this directive if you want to use it other places besides name as minlength, maxlength, and pattern can be passed to it via attributes. If they are left off, they will be ignored.

See jsfiddle: http://jsfiddle.net/4zpxk/6/

OTHER TIPS

I searched in angular code why this behavior. Then in the function 'textInputType' that it's the specific function that handles text inputs for the angular 'input' directive I found this at the end of this function, where we can see three blocks of code.

// pattern validator
if (pattern){
 //validator logic
}

// min length validator
if (attr.ngMinlength) {
 //validator logic
}

 // max length validator
if (attr.ngMaxlength) {
 //validator logic
}

So, no matter if you change the declaration order of your ng-* attributes in the html input element you will always get same result but if you change the order of the blocks, I mean, put the min length validator block before pattern validator block you will have the result that you expect.

This is a solution for your problem but you have to make a litte change in angular code and I don't know if you really like this. But you got a very common situation where order of the declaration of validation concepts matters, so, something more must be done to handle this. Thanks

You cannot change the default check order unfortunately.

One solution is to write a custom validator, not that difficult. Based on this answer, I came up with this code (fiddle)

  1. Usage: There is an array of validation functions in the scope, they get passed to our custom directive "validators" as:

    <input name="name" type="text" ng-model="name" validators="nameValidators"/>
    
  2. A validator function would look like (e.g. for the minlength constraint):

    function minlength(value, ngModel) {
        if( value == null || value == "" || value.length >= 3 ) {
            ngModel.$setValidity('minlength', true);
            return value;
        }
        else {
            ngModel.$setValidity('minlength', false);
            return;
        }
    }
    

    Important points are: it takes the value and the ngModel as arguments, performs the test (here value.length >= 3) and calls ngModel.$setValidity() as appropriate.

  3. The directive registers the given functions with ngModel.$parsers:

    app.directive("validators", function($parse) {
        return {
            restrict: "A",
            require: "ngModel",
            link: function(scope, el, attrs, ngModel) {
                var getter = $parse(attrs.validators),
                    validators = getter(scope),
                    i;
    
                for( i=0; i < validators.length; i++ ) {
                    ngModel.$parsers.push((function(index) {
                        return function(value) {
                            return validators[index](value, ngModel);
                        };
                    })(i));
                }
            }
        };
    });
    

Many details can be tweaked and improved, but the outline works (again link to fiddle). Now the order of validation is explicitly set by the order of the validator functions in the nameValidators array.

If you use ng-messages you should be able to set the order via the order of ng-message elements, e.g:

<div ng-messages="field.$error">
  <ul class="validation-errors">
    <li ng-message="required">This has the highest prio</li>
    <li ng-message="min">Second in command</li>
    <li ng-message="max">I'm last</li>
  </ul>
</div>

Also the docs on this: https://docs.angularjs.org/api/ngMessages/directive/ngMessages

i just changed the order of your directives, pattern first

<input name="name" type="text" ng-model="name" ng-pattern="/^\w+$/" ng-minlength="3" ng-maxlength="16"/>

EDIT: uuum, tested your fiddel without changes and it shows your desired behavior ... directives are compiled by priority, bbut i don't know how to set angulars directives priority ... sorry, should have tested this first

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top