4. Specific (Custom) Constraints and Validations

Generic constraint and validation can solve most of the common requirements. But when it can not settle some special problems, Angular provides another way - Custom Directive. With a custom directive, we can add our own validation functions to the $validators or $asyncValidators object on the ngModelController.

4.1. Calculated Value

Previously we implement the year's constraint and validation. The range of max should not be a constant value but a dynamic value which should be set with the value of current year. If we do not want to take a task for changing it by ourself every year, it's better to let the computer to calculate the value.

4.1.1. Fast and Simple Way

First we set an expression {{currentYear}} as the value of attribute ng-max. JavaScript has a function new Date().getFullYear() can get current year’s value, so we insert it to the applicaton configuration's file in js/app.js:

publicLibraryApp.run(['$rootScope', function( $rootScope) {
  $rootScope.currentYear = new Date().getFullYear();
}]);

Every Angular application has a single $rootScope, all other $scope is descendant scope of it. Angular will take the value from $rootScope to $scope and show it to the attribute's value {{currentYear}}. This method may not suit for large project but enough for our tiny public library project.

4.1.2. User-Defined Angular Directive

The Angular directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell Angular’s HTML compiler (it is like attaching event listeners to the HTML to make is interactive) to attach a specified behavior to that DOM element or even transform the DOM element and its children. So we can implement an user-defiend Angular directive for our special purpose.

We consider right now only the range constraint about year, replace ng-min and ng-max as one attribute pl-year:

<input type="number" name="year" ng-model="book.year"
       pl-year="pl-year" />

Here the pl-year is user-defined Angular directive. We make a prefix pl- cause of the name of our web application "Public Library". This pl-year would be implemented to constrain the book’s publish year, which should not before 1459 or later than current year. When we want to use this method, it's better that we group directives as an Angular module named "plDirectives" first, and inject this module into configuration file js/app.js:

var publicLibraryApp = angular.module('publicLibraryApp',
  [
    'ngRoute',
    'plControllers',
    'plDirectives'
  ]
);

Secondly connect the directives.js file by index.html:

<head>
  ...
  <script src="js/directives.js"></script>
</head>

Then create a file js/directives.js:

var plDirectives = angular.module('plDirectives', []);

plDirectives.directive('plYear',function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function ( scope, element, attribute, ngModel) {
      ngModel.$validators.plYear = function ( modelValue, viewValue) {
        var minYear = 1459;
        var maxYear = newDate().getFullYear();
        if(modelValue < 1459 || modelValue > maxYear) {
          return false;
        } else {
          return true;
        }
      }
    }
  };
});

Angular has its own normalization process for user-defined Angular directive: the name of directive should strip the x- or data- prefix; such :, - or _-delimited name will be converted to camelCase. So the attribute we defiend pl-year will be named plYear. Let's go deep into our directive:

  • restrict: 'A' means, the directive will only match attribute name. Besides, 'E' only matches element name, 'C' only matches class name and 'AEC' will match either attribute or element or class name.

  • require: 'ngModel' means, the directive require the ng-model, in our case ng-model="book.year".

  • link: function(scope, element, attribute, ngModel) has several parts: link takes so a function to modify the DOM. scope is an Angular scope object; element point the element which this directive matches; attribute is a key-value pairs object normalized attributes names and the corresponding values.

  • ngModel.$validators.plYear = function(modelValue, viewValue) also has several parts: $validators is a property of ngModelController, a collection of validators that are applied whenever the model value changes. modelValue is the value with an actual type, viewValue keep same content as modelValue but in the type of string.

A message bookInfo.year.$error.plYear with a boolean value true will be returned, when the publish year of a book is earlier than 1459 or later than current year.

Using this method we won't need to define a globle variable within $rootScope but just use the attribute pl-year to keep watch on the value of user-input year.

4.2. Unique Value

We store books in our web application, and each book has a mandatory value ISBN, which means in our library there should not be two or more books that have one same ISBN, it should be a unique Value.

Similar like the User-Defiend Directive created as above, we realize here again an user-defined Angular directive but an asynchron XMLHTTPRequest to our Parse cloud storage, that will check the cloud database, whether there is a book with same ISBN or not, after user filled the ISBN value. A message bookInfo.isbn.$error.plStdid with a boolean value true will be returned, when the ISBN exists in the database.

We insert here an user-defined Angular directive pl-stdid:

<input type="text" name="isbn" ng-model="book.isbn"
       pl-stdid="pl-stdid" />

and implement this directive in the file js/directives.jsFigure 6.2, “User-Defined Direcitve plStdid”. Unluckly the isbn is not the standard id in Parse cloud database, so we should use its query method with a parameter where to check whether the user-input isbn exists in the database or not:

Figure 6.2. User-Defined Direcitve plStdid

var plDirectives = angular.module('plDirectives', []);

plDirectives.directive('plStdid',
['$q', '$http', function ( $q, $http) {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function ( scope, element, attribute, ngModel) {
      ngModel.$asyncValidators.plStdid = function ( modelValue, viewValue) {
        var checkValue = modelValue || viewValue;
        $http( {
          method: 'GET',
          url: 'https://api.parse.com/1/classes/Book/',
          params: { where: { isbn: checkValue}}
        })
          .success( function ( data) {
            if ( data.results.length === 0) {
              // no book with this ISBN
              defer.resolve();
            } else {
              // ISBN exists!
              defer.reject();
            }
          })
          .error( function ( data) {
            // Error when request
            defer.reject();
            console.log( data);
          });
        // return an boolean value as result
        return defer.promise;
      };
    }
  };
}]);


  • The lines with restrict, require, link introduced this directive: this directive will only be used as Attribute, the value of conbined Angular Model will be used and this directive was linked to a function.

  • $q.defer(): is an Angular service used for asynchronous programming to expose the associated promise for signaling the successful or unsuccessful completion as well as the status of the task.

  • defer.resolve() and defer.reject(): these two methods are used to return boolean value to promise.

  • defer.promise: waites for finishing the request and throw the result.

Using this directive the isbn can be checked directly in one second after user put the value in the input-field. Acturally it's better to implement a common method to check the unique value for our web application, please download the app and check the js/directives.js file.

4.3. Display Error-Messages

After the different constraints are defined, in this step, we march towards displaying error-messages.

Angular offers several solutions, rather directives, to handle this task. The popular forms are:

  • ngShow, begins in Angular v1.0.x;

  • ngIf, begins in Angular v1.2.x;

  • ngMessages, begins in Angular v1.3.x.

In following comparison we use the year’s input field as the example:

<input type="number" name="year" ng-model="book.year" 
       ng-min="1459" ng-max="{{currentYear}}" ng-required="true" />

The input of ngModel book.year is required, the value must be a number and the number should not be less than 1459 or more than the value of current year.

4.3.1. ngShow

The ngShow directive shows or hides the given HTML element based on the expression provided to the ngShow attribute. The element is shown or hidden by removing or adding the .ng-hide CSS class onto the element. So the truth is, the content in this element will be loaded with the web page, and the CSS controll it to appear or to hidden.

Display error-messages using ngShow:

<input type="number" name="year" ng-model="book.year"
       ng-min="1459" ng-max="{{currentYear}}" ng-required="true" />
<span ng-show="bookInfo.year.$error.required">
  Year is required.
</span>
<span ng-show="bookInfo.year.$error.number">
  The YEAR should only be number like "1459" here.
</span>
<span ng-show="bookInfo.year.$error.min">
  The publish year should be after 1459.
</span>
<span ng-show="bookInfo.year.$error.max">
  The publish year should be before {{currentYear}}.
</span>

When the page is loaded, all span will also be loaded, and we can see its content “Year is required.” existed just because Angular detects that currently the input field is empty or rather the value of bookInfo.year.$error.required is true, so the content inside of span should appear. After we put some letters to the field, “Year is required.” will be hidden.

The behavior of span with attribute ng-show="bookInfo.year.$error.number", ng-show="bookInfo.year.$error.min" and ng-show="bookInfo.year.$error.max" are the same as the span with attribute ng-show="bookInfo.year.$error.required.

Figure 6.3. Display Error Messages using ngShow

Display Error Messages using ngShow

Please notice that the span is only be hidden but still exists in the DOM. This is exactly the different part between ngShow and ngIf.

4.3.2. ngIf

The ngIf directive removes or recreates a portion of DOM tree based on an expression. If the expression assigned to ngIf evaluates to a false value, then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.

Display error-messages using ngIf:

<input type="number" name="year" ng-model="book.year"
       ng-min="1459" ng-max="{{currentYear}}" ng-required="true" />
<span ng-if="bookInfo.year.$error.required">Year is required.</span>
<span ng-if="bookInfo.year.$error.number">
  The YEAR should only be number like "1459" here.
</span>
<span ng-if="bookInfo.year.$error.min">
  The publish year should be after 1459.
</span>
<span ng-if="bookInfo.year.$error.max">
  The publish year should be before {{currentYear}}.
</span>

When the page is loaded, we can see its content “Year is required.” existed. It looks like using ngShow because currently the value of bookInfo.year.$error.required is true, ngIf made a clone of span and inserted it into DOM, so the content inside of span is shown. After we put some letters to the field, the value of bookInfo.year.$error.required would be false, and ngIf will remove the span so that “Year is required.” will disappear.

The behavior of span with attribute ng-if="bookInfo.year.$error.number", ng-if="bookInfo.year.$error.min" and ng-if="bookInfo.year.$error.max" are the same as the span with attribute ng-if="bookInfo.year.$error.required.

Figure 6.4. Display Error Messages using ngIf

Display Error Messages using ngIf

ngIf controlls the elemente which is a part of the DOM tree while ngShow changes the behavior of the web site via a CSS property. On the other hand, once all directives matching DOM element have been identified, the Angular compiler will sort the directives by their priority. ngIf has one of the hightest priority (level 600 by default), it will run first before all other lower prioritised directives. And if we use it instead of ngShow, the UI will be well speeded up.

4.3.3. ngMessages

The ngMessages is an Angular module published by Angular version 1.3.x. It contains ngMessages and ngMessage directives. It specifically provides enhanced support for displaying messages, such as error messages, within templates. Instead of relying on complex ng-if statements within our form template to show and hide error messages specific to the state of an input field.

The tasks of two directives are:

  • ngMessages: is designed to show and hide messages based on the state of a key/value object that it listens on, and the directive itself compliments error message reporting with the $error object. By default, only one message will be displayed at a time, but this can be changed by using the ng-messages-multiple on the directive containter. And by using ng-messages-include the specified template can be included into the ng-messages container.

  • ngMessage: has the purpose to show and hide a particular message. For ngMessage to operate, a parent ngMessages directive on a parent DOM element must be situated since it determines which messages are visible based on the state of the provided key/value map that neMessages listens on.

AngularJS-messages.js must be included as a script element in index.html and injected as a module in js/app.js.

As an example we use ng-messages-include and create a single file errorYear.html in a new folder partials/errorMessages to store all possible error-messages about the ngModel book.year:

<!-- partials/createBook.html -->
<input type="number" name="year" ng-model="book.year"
       ng-min="1459" ng-max="{{currentYear}}" ng-required="true" />
<span ng-messages="bookInfo.year.$error"
      ng-messages-include="partials/errorMessages/errorYear.html">
</span>

<!-- partials/errorMessages/errorYear.html -->
<span class="errors">
  <span ng-message="required">Year is required.</span>
  <span ng-message="number">
    The YEAR should only be number like "1459" here.
  </span>
  <span ng-message="min">
    The publish year should be after 1459.
  </span>
  <span ng-message="max">
    The publish year should be before {{currentYear}}.
  </span>
</span>

The DOM behavior by using ngMessages is similar as by using ngIf:

Figure 6.5. Display Error Messages using ngMessages

Display Error Messages using ngMessages

But ngMessages can organize the error-messages more compactly than a bunch of statements, and be reused in other cases such as we can also invoke the error-messages for "Update Object" use case. These are the pro-point comparing with ngIf.

4.3.4. Get Message from Parse.com

We have discussed the way of displaying error-message based on our local application. By using $http Angular can also report messages received from database such as Pasre.com. Two functions will get ready once when a request is finished:

$http({...})
  .success( function ( data, status, headers, config) {
    ...
  })
  .error( function ( data, status, headers, config) {
    ...
  });

success() will be called as long as the response is available, otherwise error() will be called. The both functions can get four objects:

  • data: when the response is available, all the requeted data will be stored here. Otherwise, it will contain the error message.

  • status contains the HTTP status code.

  • headers is the header getter function.

  • config contains the whole configuration about the generated request information.