Understanding and fixing AngularJS directive rendering and parsing
NOTE: This blog post is originally written for AngularJS 1.2.x; in 1.3.x the "input not showing invalid model values" has been fixed.
Although 1.3.x still has the "inconsistencies in how AngularJS parses data entry" the solution from this blog post isn't working for 1.3.x but I will try to find a fix for this within the next few weeks.
A while ago I noticed that AngularJS doesn't show invalid model values bound to an <input/>
There is also an open bug report about this: issue #1412 - input not showing invalid model values The bug can be easily illustrated through the following example:
letters = { {'' + letters}}
While running the example displays letters = 1
but the <input/>
element remains empty.
Additionally notice that the <input/>
element (due some custom CSS styling) has a "red" background to indicate that its value is invalid (since it doesn't match the regex of ng-pattern
). In this blog post I will dig into how AngularJS handles rendering, parsing and validation and will finally provide a workaround / solution for this AngularJS bug as well as some other improvements.
Using $formatters for validating model values
In order to solve this issue we first need to understand how model values are rendered and validated. Whenever a model value bound with ngModel changes, AngularJS uses the NgModelController#$formatters array to validate and format the model value for rendering:
Array of functions to execute, as a pipeline, whenever the model value changes. Each function is called, in turn, passing the value through to the next. Used to format / convert values for display in the control and validation.
By pushing a function on the $formatters
array and using [$setValidity(validationErrorKey, isValid)](http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController#$setValidity)
in its implementation we could easily create a improved version using the following jdOnlyLetters
directive that "does" show invalid model values:
angular.module('jdOnlyLetters', [])
.directive('jdOnlyLetters', function() {
return {
require: 'ngModel',
link: function($scope, $element, $attrs, ngModelController) {
ngModelController.$formatters.push(function(value) {
var onlyLettersRegex = /^[a-zA-Z]*$/;
var isValid = typeof value === 'string'
&& value.match(onlyLettersRegex);
ngModelController.$setValidity('jdOnlyLetters', isValid);
return value;
});
}
};
});
NOTE: the sample directive doesn't format anything we only want to validate the model value.
See the documentation of $formatters for a sample formatter that formats to upper case.
Understanding the $parsers array of NgModelController
Before we can understand why the ng-pattern="/^[a-zA-Z]*$/"
isn't displaying illegal model values (while our "jdOnlyLetters" directive does) we first need to understand the [NgModelController#$parsers](http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController#$parsers)
array. The $parsers
array of NgModelController
is used for parsing as well as validation upon data entry:
Array of functions to execute, as a pipeline, whenever the control reads value from the DOM. Each function is called, in turn, passing the value through to the next. Used to sanitize / convert the value as well as validation. For validation, the parsers should update the validity state using $setValidity(), and return undefined for invalid values.
Using the $parsers
array we could improve our "jdOnlyLetters" directly to perform validation upon data entry of the user:
angular.module('jdOnlyLetters', [])
.directive('jdOnlyLetters', function() {
return {
require: 'ngModel',
link: function($scope, $element, $attrs, ngModelController) {
var onlyLettersRegex = /^[a-zA-Z]*$/;
ngModelController.$formatters.push(function(value) {
var isValid = typeof value === 'string'
&& value.match(onlyLettersRegex);
ngModelController.$setValidity('jdOnlyLetters', isValid);
return value;
});
ngModelController.$parsers.push(function(value) {
var isValid = typeof value === 'string'
&& value.match(onlyLettersRegex);
ngModelController.$setValidity('jdOnlyLetters', isValid);
return isValid ? value : undefined;
});
}
};
});
NOTE: the sample directive above doesn't actually parse nor format anything as its only interested in model value validation. The parser function is "almost" exactly the same as the formatter function. However there is "one" noticeable difference between the two, a parser function should return a undefined
in case of an invalid value. After investigating the source code of AngularJS it turns out that the "ngPattern" directive (as well other directives of AngularJS) use the "same" function as both a "parser" and "formatter" function. Since this function returns undefined
(in case of an invalid value) no value will be shown in the <input/>
element. To fix this I created a "input" directive that fix the parsing result.
This is done by adding a parser function which is always executed as the last parser function (of the 'pipeline' of parser functions).
Whenever the model value is invalid and the parsed value is undefined
this parser function will return the $modelValue instead. A fully working version of ng-pattern="/^[a-zA-Z]*$/"
using this fix can be found here.
Improving inconsistencies in how AngularJS parses data entry
Additionally to AngularJS not showing invalid model value in elements, I also noticed some inconsistencies in how it uses data:
- In case of an invalid value (due to the
undefined
returned by the "parser" function) the model value will be set toundefined
instead of anull
value:
In my opinion it's a bad practice to set variables and object property to a value ofundefined
. Instead I prefer thatundefined
is only (implicitly) set by JavaScript in case an object property doesn't exist or a variables is not initialized yet. Furthermore JSON doesn't supportundefined
, therefor (as it turns out) $resource omits anyundefined
object property when doing an HTTP PUT / POST which can cause unexpected issues when using RESTful services. - After emptying the an text of the
<input/>
element the model value is set to an empty string instead anull
value: to prevent unneeded ambiguity betweennull
and''
(an empty string), I prefer to only usenull
values instead.
A complete solution containing the fixes for parsing as well as rending values can be found here.