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:

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) in its implementation we could easily create a improved version using the following jdOnlyLetters directive that “does” show invalid model values:

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 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:

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 <input/> 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 to undefined instead of a null value:
    In my opinion it’s a bad practice to set variables and object property to a value of undefined. Instead I prefer that undefined 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 support undefined, therefor (as it turns out) $resource omits any undefined 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 a null value:

    to prevent unneeded ambiguity between null and '' (an empty string), I prefer to only use null values instead.

A complete solution containing the fixes for parsing as well as rending values can be found here.

References

13 thoughts on “Understanding and fixing AngularJS directive rendering and parsing

  1. Thanks for the detailed article!
    In the 2nd code example please fix line 20 isValue to isValid.
    Did you submit a PR to angular for the input pipelines bug with ngPattern?
    Thanks !

    • Thanks for your reply as well as the fix of the sample code.

      As for a pull request… I did gave it some thought, but didn’t get round to creating one.
      Furthermore my interest was to see if I could intervene in the formatting / parsing behavior without having to change source-code; this way I can reuse the solution for both built-in as well as third-party directives.

  2. Brilliant stuff. I like that your solution plugs into the existing angular pipeline of parsers and takes advantage of the fact that a custom directive gets the last say in the pipeline.

  3. Seems that instead of pushing you need to unshift the custom code to the $parsers array (putting it at the beginning instead of the end)

    • To make sure that the custom (parser) is executed before any pre-existing $parsers array functions (assuming they are always push-ed) we do an “unshift”.
      However in the sample “jdOnlyLetters” directive from the blog post I’m just adding a regular parser function (and not trying to fix pre-existing ones) and therefor we do a regular “push”.

    • Steffen,

      Thank you very much for your response.
      And also for your in-depth pointers…
      …I will (hopefully) find some time next week to dive into it and update the article to reflect the changes in Angular 1.3.x.

      Emil

  4. Hi

    Your first approach works fine when you provide
    ng-model-options=”{ allowInvalid: true }”

    You don’t have to create custom directive to handle that problem.

    https://docs.angularjs.org/api/ng/directive/ngModelOptions

    allowInvalid: boolean value which indicates that the model can be set with values that did not validate correctly instead of the default behavior of setting the model to undefined.

    Here you have your code with this option:
    http://plnkr.co/edit/1E6deo6Llqi2s4rLYXne?p=preview

    Marcin

  5. Is is possible to pass dynamic value(value got from java class) to ng-pattern?If yes can someone please explain how i can achive this. Thx in advance.

Leave a Reply

Your email address will not be published. Required fields are marked *