Delay validations in AngularJS. My take.
Recently a client was in need of some custom behavior for when to show the invalid state of a field/model though ng-invalid
. The client is a big company (enterprise style), so the team got dictated how the application should behave. There was no room for discussion, unfortunately.
What they needed to accomplish was:
- Don't show the invalid state (through
ng-invalid
) while typing (eg. an email address) but after the field lost focus - If the invalid state is already shown, correcting errors should immediately lead to the valid state (
ng-valid
)
After listening to the first point my first thought was to change the CSS, so that the changes of the CSS for ng-invalid
would be "reverted" when the field was currently focused. After listening to the second point we needed to come up with a different idea.
A first idea
Someone proposed to mimic the behavior of validations that has been used in the project, but trigger on blur instead of adding $parser
/$formatter
. There was no answer yet on how to solve the second point, but copying the behaviors of all used validations was no option for me. It just doesn't make sense to copy effective code and, basically, implement the same thing twice.
Additionally, when I was hired for the project, I recommended to do much more validation in the front end, so being able to simply add more validators (even custom ones) should be easily possible without having to think about when to fire: all validators should be set as $parsers
and $formatters
of the ngModel
, which is the recommended way. That should be all.
What I came up with
So my idea was: every validation happens through $parser
and $formatter
functions, which are added through directives. Maybe I could control when those are in effect and when not? In general, all I'd need is a way to jump in after all directives have been compiled and linked, and remove and add the already set functions at the right time.
So I started prototyping and testing and the result was this directive:
angular.module('myApp')
.directive('rbDelayValidation', function() {
return {
priority: 9999,
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
var parsers = [],
formatters = [],
hasParsersFormatters = true,
oldViewValue;
removeParsersFormatters();
element.on('blur', function() {
var unwatch;
if (oldViewValue !== ctrl.$viewValue) {
oldViewValue = ctrl.$viewValue;
injectParsersFormatters();
ctrl.$setViewValue(ctrl.$viewValue);
if (ctrl.$valid) {
removeParsersFormatters();
} else {
unwatch = scope.$watch(
function() { return ctrl.$valid; },
function(newValue, oldValue) {
if (newValue !== oldValue) {
removeParsersFormatters();
unwatch();
}
}
);
}
}
});
function removeParsersFormatters() {
if (hasParsersFormatters) {
while (ctrl.$parsers.length > 0) {
parsers.push(ctrl.$parsers.shift());
}
while (ctrl.$formatters.length > 0) {
formatters.push(ctrl.$formatters.shift());
}
hasParsersFormatters = !hasParsersFormatters;
}
}
function injectParsersFormatters() {
if (!hasParsersFormatters) {
while (parsers.length > 0) {
ctrl.$parsers.push(parsers.shift());
}
while (formatters.length > 0) {
ctrl.$formatters.push(formatters.shift());
}
hasParsersFormatters = !hasParsersFormatters;
}
}
}
}
});
What is it doing?
Basically, it removes all $parser
and $formatter
functions from the model and adds them again after a blur when a change to the $viewValue
has been detected. It then sets the $viewValue
again, which is kind of ugly but the only way I found to trigger the re–added functions through AngularJS (any other idea?). If the model turns to valid, all is fine and we return to the initial state (=remove all functions again). But if it's not valid, we still need to wait for more changes until the model turns valid, then return to the initial state.
With this behavior, this directive helps to comply with the demands made by the client. Additionally this behavior would be better in regards of usability, since it doesn't alert the user while typing, even before he reached "the end" of his input. (Actually, IMHO, it would be great when the form responses with some kind of "we're not there yet" info, eg. with an orange border instead of a red one or none; but this was not the requirement here.)
How to use it
Just add it to a field with some other directive attributes or directives:
<input ng-model="foo.bar" name="foobar" ng-minlength="10" rb-delay-validation>
Because of the priority of the rbDelayValidation directive, all $parser
and $formatter
should already be set. If you're writing custom directives, make sure they have a priotity set to something below 9999 or change the rbDelayValidation directive :-).
What to do better
This solution is not the best, yet it's the only one I came up with that fits the demands and the environment of my client. I'd be glad to know of a way to initiate the calling of all $parser
and $formatter
functions though AngularJS without re–setting the $viewValue
, because it triggers all $viewChangeListener
again.
Additionally I'd like to hear any critic or risks that I may have missed. I'd appreciate any thoughts and ideas!
Things that one could add, too:
- only remove/add parsers, so that model changes from outside are immediately reflected though the
ng-valid
/ng-invalid
class - add a "we're not there yet" state, so that the user knows that the input is not ready yet (useful for IBAN numbers or different syntax specific input).
Thanks :-).
P.S.: All gets better with AngularJS 1.3 and ngModelOptions ;-)
Written by Matthias Dietrich
Related protips
1 Response
Clever idea! I've just stumbled similar problem and I'm trying to modify your directive - but I cannot get it to work. Is it not working in 1.4 or am I doing something wrong?
The problem I need to solve is that I need to show delayed error messages (after user stops typing), but... if the field becomes valid while user is still typing it should be immediately marked valid to the user (with no delay!) - may you have any idea how to approach that...? Any help would be greatly appreciated. Thanks for sharing your ideas in here!