Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

$asyncValidators run multiple times per element on load (given ng-repeat || ng-(min/max)length)) #14691

Closed
changhaitravis opened this issue May 28, 2016 · 14 comments · Fixed by #16760

Comments

@changhaitravis
Copy link

Note: for support questions, please use one of these channels: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#question. This repository's issues are reserved for feature requests and bug reports.

Do you want to request a feature or report a bug?

Bug

What is the current behavior?

If you have a model defined already and it's put into an input text element with a custom validator defined and this input field is in an ng-repeat, then, on the page load, the $ayncValidator will be run once, and then run in quick succession run again for every validator directive such as "minlength, maxlength, ng-maxlength, ng-minlength, required".

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (template: http://plnkr.co/edit/tpl:yBpEi4).

  1. define a model in the controller
  2. define a custom directive that has an $asyncValidator linked (put in a console.log before returning the promise)
  3. create a div with ng-repeat (if this step is omitted, then the problem is only exhibited with ng-maxlength and ng-minlength)
  4. Inside of the ng-repeat clause create an input type="text" element, and load it up with maxlength, minlength, and required
  5. take note of how many times the console.log() that was set up in $asyncValidator is tripped when you hit "Run".
  6. when you go and interact with the element, then it behaves normally and as expected.

http://jsfiddle.net/yofk7u0L/5/

What is the expected behavior?

$asyncValidators only fire once upon directive initialization with a non-empty model, regardless of how many other attribute directives that are validators exist in the element, also regardless of whether they're in ng-repeat or not.

What is the motivation / use case for changing the behavior?

In the application I'm currently working on, a user can have up to 10 of these $asyncValidator input fields loaded up at the same time. With minlength, maxlength, and required enabled on each, this makes 30 EXTRA requests to my API, on top of the 10 that were welcome. If I use ng-minlength and ng-maxlength, it could bump it up an extra twenty (two per input element)

Which versions of Angular, and which browser / OS are affected by this issue? Did this work in previous versions of Angular? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.

Firefox and Chrome on Angular 1.5.3, 1.5.5, 1.5.6 and v1.5.7-build.4838+sha.cfc8b41 exhibit this behavior.
I'm seeing this happen on both Windows 7 (1.5.3 & 1.5.5) and Linux Fedora 23 KDE (All the above)

Other information (e.g. stacktraces, related issues, suggestions how to fix)
Not sure how to fix this, but I guess:

  • Order directives are placed have no bearing
  • Synchronous $validators within the same directive as the $asyncValidators behave normally.
  • The entire list of $asyncValidators seems to be looped through each time
  • I have not tried adding custom directives to the input element, but I suspect that they might behave similarly and contribute to the amount of times
  • If you use a combination of ng-repeat and ng-minlength and ng-maxlength that seems to maximize the total number of requests.
  • Have not tested to see if other input types are affected
@pocesar
Copy link
Contributor

pocesar commented Jun 4, 2016

that's expected, I'm pretty sure. that's how the digest cycle works. it needs to make sure that the value hasn't changed and is 'stable'. you could make your async validator check for a presence of a viewValue on the model, so it would return only when it's present, and reject the validation right away if it doesn't

@changhaitravis
Copy link
Author

That would have been an acceptable workaround, but in this scenario, viewValue is already present when the asyncValidator fires its (extra) requests. The only workaround I suspect works is to package $asyncValidator in the same directive along with all of the other directive's functionality (minlength, maxlength, required) that I want to use.

@Narretz
Copy link
Contributor

Narretz commented Jun 6, 2016

I wouldn't quite say it's expected behavior, since it works as expected without ngRepeat. There are two issues that play together:

  • required, maxlength, minlength call $validate whenever their obsever (for attribute changes) fires. This happens also initially after the element is compiled and a $digest happens. Without ngRepeat, the model hasn't been initialized yet, so no validators actually run
  • ngRepeat terminates the compilation and appens the elements with the $transclude function during the first $digest. This causes a difference to the no-repeat scenario: With ngRepeat, the $digest happens before the elements are linked, and the ngModel is initalized. Without ngRepeat, the $digest happens after the elements have been linked and and the observers fire when the ngModel isn't intialized yet.

This is honestly pretty weird, because I would expect ngModel to be validated once after the first $digest without ngRepeat, and it's never validated. With ngRepeat, it's validated once and 1 time per validation directive. Both cases are wrong, imo, but I suspect the first case doesn't appear too often, since on load usually more than one digest happens.

@krutkowski86
Copy link

+1

@NaomiN
Copy link

NaomiN commented Sep 7, 2017

I am running into exactly the same problem. In fact, I only want all my validators to only fire when I changed the value and not right away. Is there a way for this? Say, I tried adding ngModel.$touched check in the directive, but then I could not fire it at all. What is the best workaround to only fire validations when the value is changed?

I am using AngularJs 1.6.4 and seeing this behavior in Google Chrome

@hdi-amiri
Copy link

I am having the same issue any update on this issue? or any workaround? I am using angular 1.5.11 and I have my async validator with ng-required and two custom validator of my own.

Regards

@NaomiN
Copy link

NaomiN commented Oct 18, 2017

Hi,

I think I was able to prevent running it too many times on load but I'm not 100% certain as I then found that it was not running all the times when I needed, so I made few extra changes lately. I plan to write a TechNet article that explains my solution in all the details, I'll post the link once I do that.

@krutkowski86
Copy link

@NaomiN +1
I'm looking forward to hearing from you.

@Narretz Narretz modified the milestones: Backlog, 1.6.x Oct 18, 2017
@NaomiN
Copy link

NaomiN commented Oct 22, 2017

I just posted that article - will appreciate questions and suggestions to improve

https://social.technet.microsoft.com/wiki/contents/articles/42394.implementing-server-side-validations-in-angularjs.aspx

@hdi-amiri
Copy link

I solved(?) my issue with the following approach:
for extra call on validation service I send the unfulfilled promise, I would like to know opinion of you guys about it.

  function asyncValidator(myValidatonService, $q) {
        'ngInject';
        return {
            require: 'ngModel',           
            restrict: "A",
            link: linkFn
        };
        function linkFn(scope, elem, attrs, ngModel) {
            if (!ngModel) return;
            var cache = {};
            ngModel.$asyncValidators.myAsyncValidator= function (viewValue, modelValue) {
                var value = viewValue || modelValue;
                if (ngModel.$isEmpty(value))
                    return $q.resolve();            
                if (cache[value])
                    return cache[value];
                if (!cache[value]) {
                    cache[value] = myValidatonService.isValid(value)
                        .then(function (response) {                           
                            cache[value] = false;
                            return $q.resolve();
                        })
                        .catch(function (error) {                          
                            cache[value] = false;
                            return $q.reject();
                        });
                    return cache[value];
                }
            }
        }

@adubadzel
Copy link

adubadzel commented Jan 18, 2018

@ha0x7c7 Just to mention,
In case if you use $http for validation:
https://docs.angularjs.org/api/ng/service/$http#caching works for me better, and it is simplier. !Note Works with GET and JSONP requests.
Additionally, you can specify your own cache object and clear cache on some event.
As example:

(function () {
	'use strict';
	angular.module('app').factory('specificCache', specificCache);
	specificCache.$inject = ['$cacheFactory'];

	function specificCache($cacheFactory) {
		return $cacheFactory('specificCache');
	}
})();

(function () {
	'use strict';
	angular.module('app').factory('validationService', validationService);
	validationService.$inject = ['$http', 'specificCache'];
	function validationService($http, specificCache) {
 		const service = {
			validate: validate
		}
		return service;

		function validate(params){
			return $http.get('someUrl', {
				params: params,
				cache: specificCache
			});
		}
	}
})();

(function () {
	'use strict';
	angular.module('app').directive('myAsyncValidator', myAsyncValidator);
	myAsyncValidator.$inject = ['$q', 'validationService'];
	function myAsyncValidator($q, validationService) {
		const directive = {
			link: link,
			restrict: 'A',
			require: 'ngModel'
		};
		return directive;

		function link(scope, element, attrs, ngModel) {
			ngModel.$asyncValidators.myAsyncValidator = function (modelValue, viewValue) {
				const value = viewValue || modelValue;
				if (ngModel.$isEmpty(value)) {
					return $q.resolve();
				}
				
				return validationService.validate(value);
			}
		}
	}
})();

(function () {
	'use strict';
	angular.module('app').controller('someController', someController);
	someController.$inject = ['specificCache'];
	function someController(specificCache){
		var vm = this;

		vm.someFunction = function(){
			specificCache.removeAll();
		};	
	}
})();

@hdi-amiri
Copy link

@adubadzel I didn't know about that feature back then, but I should mention that I didn't cache the resolved result, I cached the promise and not the value.

@adubadzel
Copy link

@ha0x7c7, Thanks for reply, now I see that our solutions work differently and can be used for different use cases.

@Narretz Narretz modified the milestones: 1.6.x, 1.7.x Apr 12, 2018
@petebacondarwin petebacondarwin modified the milestones: 1.7.x, 1.7.x - won't fix May 16, 2018
@Narretz Narretz modified the milestones: 1.7.x - won't fix, 1.7.x Oct 3, 2018
@Narretz
Copy link
Contributor

Narretz commented Oct 24, 2018

While workarounds exist, I think this is an interesting issue, and tried to get to the bottom of it.
What I found is that there seems to be a quirk with the directive property transclude (which is used by ngRepeat as element, but it also happens with simply true`) + calling the $transclude function in a watch actionFn that changes the order of some actions.
We basically have two points where validation happens in these examples:

  1. when the ngModelController propagates the scope value to the input, it calls $validate
  2. when the built in directives detect that their value (e.g. minlength) has changed ($observe), they call $validate

In normal circumstances, $compile sets up the observers, which are also fired on init. However, since the modelValue isn't set up yet, $validate doesn't do anything. Then, the ngModelWatch runs, and sets the modelValue, and calls $validate itself.

When watch+transclude comes into the mix, the whole process is reversed, so that when the observers are initially called, the modelValue is set, and $validate is called again.

However, the whole problem also happens without ngRepeat if you use interpolation for one of the minlength etc. directives, see: plnkr.co/edit/b4tY6u4u7o6QJJ7scBpD?p=preview
In this case, it definitely feels technically correct though.

I think we can fix this for the built-in validators by storing the initial attribute value, and only calling validate if the the observed new value is actually different.

Narretz added a commit to Narretz/angular.js that referenced this issue Nov 15, 2018
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen after compilation in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes angular#14691
Narretz added a commit to Narretz/angular.js that referenced this issue Nov 15, 2018
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen after compilation in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes angular#14691
Narretz added a commit to Narretz/angular.js that referenced this issue Nov 15, 2018
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen after compilation in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes angular#14691
Narretz added a commit to Narretz/angular.js that referenced this issue Dec 5, 2018

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen after compilation in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes angular#14691
Narretz added a commit that referenced this issue Dec 5, 2018
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen on initial linking of the directives in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes #14691 
Closes #16760
Narretz added a commit that referenced this issue Dec 5, 2018
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen on initial linking of the directives in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes #14691 
Closes #16760
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.