3

Why does adding additional AngularJS validation directives cause $asyncValidators to run multiple times on page load?

I created a custom directive which implements $asyncValidators. This is the basic structure of that custom directive:

myApp.directive('userSaved',['$q','userLookup',function($q, userLookup){
return {
  restrict: 'A',
  require: 'ngModel',
  link: function(scope, elem, attrs, ctrl){
    ctrl.$asyncValidators.userSaved = function(modelValue, viewValue) {
      // do stuff 
    }
  }
}
}]);

The controller initializes the tailNumber model value like this:

$scope.tailNumber = 'N33221';

This is the html where the user-saved directive runs 3 times on page load:

<input ng-model="tailNumber" name="tailNumber"   user-saved 
    ng-minlength="2"   ng-pattern="/^[A-z][a-zA-Z0-9]*$/" >

When I remove ng-minlength="2" then the user-saved directive runs twice on page load (2 times). This is the html with ng-minlength="2" removed:

<input ng-model="tailNumber" name="tailNumber"   user-saved 
    ng-pattern="/^[A-z][a-zA-Z0-9]*$/" >

When I remove ng-pattern="/^[A-z][a-zA-Z0-9]*$/" then the user-saved directive only runs 1 time. This is the html after removing ng-pattern="/^[A-z][a-zA-Z0-9]*$/"

<input ng-model="tailNumber" name="tailNumber" user-saved >

Why does my function registered with $asyncValidators run an additional time for each additional ng validator attached to the form element?

My custom directive is an expensive $http call, and I prefer my custom directive only run once on page load. Is it possible to use all of these ng validators and while only running my async validator function one time instead of 3 times on page load?

steampowered
  • 11,809
  • 12
  • 78
  • 98

3 Answers3

4

This is because validation directives like ngMaxlength, ngPattern invoke an initial validation cycle with a call to ngModelController.$validate().

This causes all the validation directive to run their validation logic, including the async validators.

One way to prevent the redundant $http calls, and in fact it is a good practice anyway, is to cache the validation result for each input.

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • The documentation says ngModelController will run _first synchronous validators and then asynchronous validators_. So why is ngModelController running my asynchronous directive while running the synchronous `ngMinlength` and `ngPattern`? – steampowered Jul 31 '15 at 14:59
  • @steampowered, right - first sync, and if they succeed, then async... The sync validators clearly succeed (which [they](https://github.com/angular/angular.js/blob/v1.4.3/src/ng/directive/validators.js#L47) [do](https://github.com/angular/angular.js/blob/v1.4.3/src/ng/directive/validators.js#L68) for empty values, for example), then async validators run – New Dev Jul 31 '15 at 15:33
  • @steampowered, did this address your question? – New Dev Aug 10 '15 at 08:45
  • The answer was helpful but I think there might be more going on with this issue. The `ngMinlength` and `ngPattern` should run synchronously before the ansyn validator runs, which is not happening. I ended up removing the `ngMinlength` and `ngPattern` validators because they didn't play nicely with an `$asyncvalidators` implementation (I removed all the code in the directive, but still my bare directive caused problems with the `ngMinlength` and `ngPattern` validators). So I have working code, but I think there may be more going on here than this answer addresses. – steampowered Aug 10 '15 at 15:33
  • @steampowered, what makes you say that `ngMinlength` and `ngPattern` do not run synchronously? They do. If you are uncertain, put breakpoints in their src code (e.g. [here](https://github.com/angular/angular.js/blob/v1.4.3/src/ng/directive/validators.js#L84)). – New Dev Aug 10 '15 at 21:59
  • It was my understanding from reading the docs that non-async validators such as ngMinlength and ngPattern would not call asyncrhonous validators. The docs seemed to indicate all synchronous validation would complete prior to asynchronous validation being attempted. However, this is not the case apparently. Anyway, your answer was helpful and did add to my understanding. I am marking your answer correct now. – steampowered Aug 11 '15 at 03:57
  • @steampowered, we might be talking about the same thing in different terms. `ngModel` calls sync validators - if they all pass, then it will call async validators. Any directive that has `require: "ngModel"` can call `ngModel.$validate()` to start the validation pipeline from the beginning, and `ngMinlength` and other validators do exactly that in their `attrs.$observe`. In fact, you have to account for that fact, because in the future someone might add another directive with `require: "ngModel"` in addition to your async validator, which is why I said that caching is anyway a good practice – New Dev Aug 11 '15 at 04:01
  • I did end up caching after you suggested it, thanks for the tip. I'll look into `ngModel.$validate()` – steampowered Aug 11 '15 at 14:07
  • Any news on this? I'm running on the same situation – endamaco Aug 12 '15 at 14:15
  • How would you cache the validation result? – endamaco Aug 12 '15 at 18:39
  • 2
    @endamaco, like you would cache anything else... you store the input and the validation result in some in-memory data-structure, like a hash – New Dev Aug 12 '15 at 20:37
  • Is there a way to only validate when something was changed? Why do we need to run validators on the form load? I only want to run validations if I changed the value. Is there a way to achieve that? – Naomi Sep 07 '17 at 17:22
  • Can you please show an example? Would overwriting the $validators.maxlength help? – Naomi Sep 08 '17 at 18:10
3

It actually took me a while to figure this one out. As mentioned in this post, Angular validators trigger additional validations. I decided not to fight this behavior and work around it instead, falling back to parsers and formatters:

myApp.directive('userSaved',['$q','dataservice',function($q, dataservice){
return {
  restrict: 'A',
  require: 'ngModel',
  link: function(scope, elem, attrs, ctrl){
    ctrl.$parsers.unshift(checkUserSaved);
    ctrl.$formatters.unshift(checkUserSaved);

    function checkUserSaved(value){
        ctrl.$setValidity("usersaved") // the absence of the state parameter sets $pending to true for this validation
        dataservice.getUserSaved(value).then(function(response){
            var userIsSaved = (response === true);

            ctrl.$setValidity("usersaved", userIsSaved); // the presence of the state parameter removes $pending for this validation

            return userIsSaved ? value : undefined;
        });

        return value;
    }
  }
}
}]);

As a reference, you also might want to check the Angular docs

EDIT

Upon further investigation, it appears that in the case of ng-pattern the extra validations are only triggered when the regex is converted from a string.

Passing the regex directly:

<div ng-pattern="/^[0-9]$/" user-saved></div> 

fixed the problem for me while making use of the validators pipeline.

For reference, see this github issue

Community
  • 1
  • 1
Tegyr
  • 129
  • 1
  • 6
1

I followed @New Dev's advice and implemented a simple caching routine which fulfilled my requirement quite nicely, here's what I came up with ..

link: function (scope, element, attributes, ngModel) {

        var cache = {};
        ngModel.$asyncValidators.validateValue = function (modelValue) {
            if (modelValue && cache[modelValue] !== true) {
                return MyHttpService.validateValue(modelValue).then(function (resolved) {
                    cache[modelValue] = true; // cache 
                    return resolved;
                }, function(rejected) {
                    cache[modelValue] = false;
                    return $q.reject(rejected);
                });
            } else {
                return $q.resolve("OK");
            }
        };
    }
svarog
  • 9,477
  • 4
  • 61
  • 77