Last Updated: February 25, 2016
·
3.58K
· biowo

Validation in Angular

Most of the related user-input-datas can be collected in the HTML5 <form>.

Angular rewrites the form directive, makes it to have a holistic mechanism for validation, so that the user can be notified of invalid inputs for example. If we want to use these feature from Angular, we should take some specific attributes in side of form and its subtag.

Based on our Public Library Web Application, we take the function of Add a book as an example to introduce the Angular-way's validation.

As we said, all of the user's inputs will be compacted into <form>, here is the basic format of HTML5 form tag in Angular:

<form name="bookInfo" novalidate="novalidate" ng-submit="createBook()">
    ...
</form>

novalidate is used to disable browser's native form validation, because every browser may have its own way to deal with form validation. That is not what we expect.

Please note that, the form is no more the orginal HTML5 tag, it has been replaced by Angular and has an alias ngForm. Which means the following two formats are the same:

1. <form name="bookInfo">...</form>
2. <ng-form name="bookInfo">...</ng-form>

form in Angular is be tracked by form.FormController, which keeps watching on the state of form, such as being valid/invalid. Besides, when the name attribute of <form> is specified, the form will be published onto the current scope under this name.

Generic Constraints and Validations

  • The form's name is "bookInfo";
  • A book have four attribute: ISBN, Title, Year and Edition. We make each as a input element to receive user’s inputs.
  • Each input has an ngModel attribute, will be controlled by Angular as a model.

We take our focus on input at this moment. Angular provides many arguments in input for data-binding, state control and validation:

  • Mandatory Value
    • Constraint: <input type="text" name="isbn" ng-model="book.isbn" required="required" />
    • Validation: bookInfo.isbn.$error.required
    • Description: the input of ngModel book.isbn is required, it should not be empty, otherwise Angular will throw an error, the value of bookInfo.isbn.$error.required will be true.
  • Pattern
    • Constraint: <input type="text" name="isbn" ng-model="book.isbn" ng-pattern="/\b\d{9}(\d|X)\b/" />
    • Validation: bookInfo.isbn.$error.pattern
    • Description: the input of ngModel book.isbn must meet the /\b\d{9}(\d|X)\b/ pattern, otherwise Angular will throw an error, the value of bookInfo.isbn.$error.pattern will be true.
  • String Length
    • Constraint: <input type="text" name="title" ng-model="book.title" ng-maxlength="50" />
    • Validation: bookInfo.title.$error.maxlength
    • Description: the input’s string length of ngModel book.title must be less than 50, otherwise Angular will throw an error, the value of bookInfo.title.$error.maxlength will be true.
  • Range
    • Constraint: <input type="number" name="year" ng-model="book.year" min="1459" max="{{currentYear}}" />
    • Validation: bookInfo.year.$error.number && bookInfo.year.$error.min && bookInfo.year.$error.max
    • Description: the input of ngModel book.year must be a number, the number also must be between 1459 and current year (talk about this later), otherwise Angular will throw corresponded error.

Besides, to allow styling of form, ngModel will automatically adds some CSS classes:

  • ng-valid: the input is valid
  • ng-invalid: the input is invalid
  • ng-touched: the input-field has been blurred
  • ng-untouched: the input-field has not been blurred
  • ng-pending: any $asyncValidators are unfullfilled
  • ...

Furthermore, the form has also some attributes for the holistic state validation. We take most useful one $invalid as an example:

<form name="bookInfo" ng-submit="createBook()">
    ...
    <input type="submit" ng-disabled="bookInfo.$invalid" />
</form>

When all of the inputs which are belong to this form are valid, the value of bookInfo.$invalid will be false (at the sametime, bookInfo.$valid would be true), then the whole datas can be submitted.

Specific (Costom) Constraints and Validations

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

Calculated Value

Previously we implement the year's constraint and validation. The range of max is a dynamic value that should be increated every year. If we do not want to take a task for changing the value by ourself, it's better to let the computer go this.

Simple Way

As our example, we set an expression {{currentYear}} as the value of max attribute. We know that new Date().getFullYear() can get current year’s value, so we put it to the controller and assign it to the scope like $scope.currentYear = new Date().getFullYear();. Angular will take the value to the attribute.

Better Way

The first method is easily and quickly, but it will mix the structure of our web application. A DOM’s problem should be better to be solved in template side, and this is exactly the purpose of implementing Angular directives.

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.

We modify a little our originally definition of HTML input-field about year:

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

Here the year-period is user-defined Angular directive. It would be implemented to constrain the book’s publish year — not before 1459 and not later than current year:

app.directive('yearPeriod', function() {
  return {
    restrict: 'A',  // the directive will only match attribute name
    require: 'ngModel',  // the directive require the ng-model
    link: function(scope, element, attribute, ngModel) {  // "link" is defined by Angular to modify the DOM
      ngModel.$validators.yearPeriod = function(modelValue, viewValue) { // “$validators” is a property of ngModelController, also a collection of validators that are applied whenever the model value changes.
        var minYear = 1459;  // Minimal publish year is 1459
        var maxYear = new Date().getFullYear();  // Maximal publish year is this year
        if (modelValue < 1459 || modelValue > maxYear) {
          return false;  // return to DOM a boolean value “bookInfo.year.$error.yearPeriod === true”
        } else {
          return true;  // return to DOM a boolean value “bookInfo.year.$error.yearPeriod === false”
        }
      }
    }
  };
});

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. The ISBN should be the unique Value.

We define the input of isbn first:

<input type="text" name="isbn" ng-model="book.isbn" ng-pattern="/\b\d{9}(\d|X)\b/" required="required" unique-isbn="unique-isbn" />

Similar like Calculated Value Directive created as above, we realize here but an asynchron XMLHTTPRequest to our Parse cloud storage, which will check the database, wheather there is a book with same ISBN or not, after user filled the ISBN input-field. A boolean value “bookInfo.isbn.$error.uniqueIsbn === true” will be returned, if the ISBN exists in the database.

publicLibraryDirectives.directive('uniqueIsbn', ['$q', '$timeout', '$http', function($q, $timeout, $http) {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, element, attribute, ngModel) {
      ngModel.$asyncValidators.uniqueIsbn = function(modelValue, viewValue) {
        var defer = $q.defer();  // $q.defer() is used to expose the associated Promise for signaling the successful or unsuccessful completion as well as the status of the task
        $timeout(function() {  // $timeout is Angular’s wapper for window.setTimeout
          $http({  // Send a GET request to Parse.com
            method: 'GET',
            url: 'https://api.parse.com/1/classes/Book',
            params: {
              where: {'isbn': modelValue}
            },
            headers:{
              'X-Parse-Application-Id': Application_ID,
              'X-Parse-REST-API-Key': REST_API_Key,
            }
          })
          .success(function(data, status, headers, config){  // When request is success, get response message from server
            if (data.results.length === 0) {  // If there is no same ISBN in the database, the response’s array should be null
              defer.resolve();
            } else {  // otherwise the ISBN exists
              defer.reject();
            }
          })
          .error(function(data, status, headers, config){  // The connection with database went wrong
            console.log("something went wrong...");
          });
        }, 2000);  // Set a time delay
        return defer.promise;  // return a boolean value to view side after finished the XMLHTTPRequest
      }
    }
  };
}]);

Different way to display Error-Messages

In this step, we march towards the different ways to display error-messages in the view.

Angular offers several solutions, or rather directives, to handle this task, the popular forms are ngShow, ngIf and a new directive ngMessages in Angular v1.3.x

We have discussed a lot about the input constraints and validations above, please be reminded that Angular will return a boolean value to a system-defined ngModel.$error variable for each ngModel validation which depends on the constraint.

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

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

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

ng-show

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" year-period="year-period" required="required" />
<span ng-show="bookInfo.year.$error.required">Year is required.</span>
<span ng-show="bookInfo.year.$error.number">Not valid number.</span>
<span ng-show="bookInfo.year.$error.yearPeriod">The publish time should be between 1459 and this year.</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.

behavior-ngShow

The span with attribute ng-show="bookInfo.year.$error.number" and ng-show="bookInfo.year.$error.yearPeriod" are similar as the span with attribute ng-show="bookInfo.year.$error.required.

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.

ng-if

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" year-period="year-period" required="required" />
<span ng-if="bookInfo.year.$error.required">Year is required.</span>
<span ng-if="bookInfo.year.$error.number">Not valid number.</span>
<span ng-if="bookInfo.year.$error.yearPeriod">The publish time should be between 1459 and this year.</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.

behavior-ngIf

The span with attribute ng-if="bookInfo.year.$error.number" and ng-if="bookInfo.year.$error.yearPeriod" are similar as the span with attribute ng-if="bookInfo.year.$error.required".

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[^1] .

[^1]: I haven’t found a tool to test the speed, the information came from a post of stackoverflow by gjoris.

ng-messages

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.

Besides angular-messages.js must be included first, before we want to use it.

Display error-messages using ngMessages:

<!— partials/createBook.html —>
<input type="number" name="year" year-period="year-period" ng-model="book.year" required="required" />
<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">Required! In which year had this book been published?</span>
  <span ng-message="number">The YEAR should only be number like 2015 here.</span>
  <span ng-message="yearPeriod">The publish time should be between 1459 and this year.</span>
</span>

The DOM behavior by using ngMessages is similar as by using ngIf. In our web application we use ng-messages-include and create a single errorYear.html file specific with all of the messages for the possible errors, so that the whole project can be structured more compactly than a bunch of statements. This is the pro-point comparing with ngIf.

behavior-ngMessages