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

shadow-left