Monday 27 October 2014

Form Validation and Displaying Error Messages using ngMessages in AngularJS 1.3

AngularJS 1.3 was released around two weeks back. As the core Angular team says, it is the best AngularJS released till date. It comes with a bunch of new features and performance improvements.

One of the key changes in this release is the changes made to forms. The directives form and ngModel went through a number of changes to make it easier to perform validation. The framework has a new module, ngMessages that makes the job of displaying validation messages easier.

In the current release, FormController have following additional APIs:

  • $setUntouched(): A method that sets all controls in the form to untouched. It is good to call this method along with $setPristine()
  • $setSubmitted(): A method that sets the state of the form to submitted
  • $submitted: A boolean property that indicates if the form is submitted

NgModelController has the following additional APIs:

  • $setUntouched(): A method that sets the control to untouched state
  • $setTouched(): A method that sets the control to touched state
  • $validators: A list of synchronous validators that are executed when $validate method is called
  • $asyncValidators: A list of asynchronous validators that are executed when $validate method is called
  • $validate(): A method that executes all validators applied on the control. It calls all synchronous validators followed by asynchronous validators
  • $touched: Boolean property that indicates if the control is touched. It is automatically set to true as soon as cursor moves into the control
  • $untouched: Boolean property that indicates if the control is untouched

For more details on these APIs, read the official API documentation of FormController and NgModelController.

Let’s see these APIs and the new featured in action. Consider the following form:


<form name='vm.inputForm' ng-submit='vm.saveNewItem()' novalidate>
  Item Name: <input name='itemName' type="text" ng-model='vm.newItem.name' required non-existing-name />
  <br />
  Min Price: <input name='minPrice' type="text" ng-model='vm.newItem.minPrice' required ng-pattern='vm.numberPattern' />
  <br />
  Max Price: <input name='maxPrice' type="text" ng-model='vm.newItem.maxPrice' required ng-pattern='vm.numberPattern' greater-than='vm.newItem.minPrice' />
  <br />
  Quantity Arrived: <input name='quantity' type="text" ng-model='vm.newItem.quantity' ng-pattern='vm.numberPattern' />
  <br />

  <input type="submit" value="Save Item" />
</form>

Note: Notice name of the form in the above snippet. It is not the way we usually name the HTML elements. We assigned a property of an object instead of a plain string name. Reason for using this is to make the form available in the controller instance. This approach is very useful in case of controllerAs syntax. (Credits: Josh Caroll’s blog post)

The form has some built-in validation and two custom validations. Built-in validations work the same way as they used to in the earlier versions ( Refer to my old blog post that talks a lot about the validations). We will write the custom validations in a minute.

Following is the controller of the page. As stated above, I am using “controller as” syntax, so no $scope in the controller.


var app = angular.module('formDemoApp', ['ngMessages']);

app.controller('SampleCtrl', function(){
  var vm = this;

  vm.inputForm = {};

  vm.numberPattern = /^\d*$/;

  vm.saveNewItem = function(){
    vm.newItem={};
    vm.inputForm.$setPristine();
    vm.inputForm.$setUntouched();
  };
});

Custom Synchronous Validations

The process of defining custom validations has been simplified in AngularJS 1.3 with $validators and $asyncValidators. We don’t need to deal anymore with $setValidity() to set validity of the control.

In case of synchronous validations, we need to return a boolean value from the validator function. The validation is passed when result of the validator function is true; otherwise, it fails.

In the form, we are accepting min price and max price values. The form should validate that the value of min price is always less than value of max price. Following is the directive for this validation:


app.directive('greaterThan', function(){
  return {
    restrict:'A',
    scope:{
      greaterThanNumber:'=greaterThan'
    },
    require: 'ngModel',
    link: function(scope, elem, attrs, ngModelCtrl){
      ngModelCtrl.$validators.greaterThan= function(value){
          return parseInt(value) >= parseInt(scope.greaterThanNumber);
      };

      scope.$watch('greaterThanNumber', function(){
        ngModelCtrl.$validate();
      });
    }
  };
});

If the above validation fails, it sets invalid flag to greaterThan validation on the control. Name of the method set to $validators is used as the validator string.

Custom Asynchronous Validations

In some scenarios, we may have to query a REST API to check for validity of data. As AJAX calls happen asynchronously, the validator function has to deal with promises to perform the task.

For the demo, I created a dummy asynchronous method inside a factory that checks for existence of the new item name in a static array and resolves a promise with a boolean value. Following is the service:


app.factory('itemsDataSvc', ['$q',function($q){
  var itemNames=['Soap', 'Shampoo', 'Perfume', 'Nail Cutter'];

  var factory={};

  factory.itemNameExists=function(name){
    var nameExists = false;
    itemNames.forEach(function(itemName){
      if(itemName === name){
        nameExists=true;
      }
    });
    return $q.when(nameExists);
  };

  return factory;
}]);

The difference between synchronous and asynchronous validators is the API used to resister the validator and the return value of the validator method. As already stated, $asyncValidators of NgModelController is used to register the validator and the validator method has to return a promise. The validation passes when promise is resolved and the validation fails if the promise is rejected.

Following is the custom asynchronous validator that checks for unique name:


app.directive('nonExistingName', ['itemsDataSvc','$q',function(itemsDataSvc, $q){
  return {
    restrict:'A',
    require:'ngModel',
    link: function(scope, elem, attrs, ngModelCtrl){
      ngModelCtrl.$asyncValidators.nonExistingName = function(value){
        var deferred = $q.defer();

        itemsDataSvc.itemNameExists(value).then(function(result){
          if(result){
            deferred.reject();
          }
          else{
            deferred.resolve();
          }
        });

        return deferred.promise;
      };
    }
  };
}]);

Validation Error Messages using ngMessages

Displaying validation messages of the form in AngularJS had not been too good earlier. It used to take a lot of mark-up and conditions to make them look user-friendly. AngularJS 1.3 has a new module, ngMessages that simplifies this task. If you refer to the module definition statement in the controller script, it has a dependency on ngMessages module. This module doesn’t come by default as part of the core framework, we have a separate file for this module.

The ngMessage module contains two directives that help in showing messages:

  • ngMessages: Shows or hides messages out of a list of messages
  • ngMessage: Shows or hides a single message

The directives in ngMessages module support animations as well. I will cover that in a future post.

In case of displaying form validation messages, ngMessages is set to the $error property of the form object and every occurrence of ngMessage element is set to the condition for which the message is has to be displayed.


<div ng-if='vm.inputForm.$dirty &amp;&amp; vm.inputForm.$invalid' ng-messages='vm.inputForm.$error' class='error-messages'>
  <div ng-message='required'>One/more mandatory fields are missing values</div>
  <div ng-message='pattern'>Data is in incorrect format</div>
  <div ng-message='greaterThan'>Invalid range</div>
  <div ng-message='nonExistingName'>Name already exists</div>
</div>

By default, ngMessages displays the first message out of the list even if more than one message is relevant. It can b overridden using multiple or ng-messages-multiple attribute on the ngMessages directive.

The above message list is generic to the form; the messages are not specific to any of the control in the form. To display messages specific to form, you can use the $error property on the form element.


<div ng-if='vm.inputForm.itemName.$dirty' ng-messages='vm.inputForm.itemName.$error' class='error-messages'>
  <div ng-message='required'>One/more mandatory fields are missing values</div>
  <div ng-message='nonExistingName'>Name already exists</div>
</div>

You can play with the sample on Plnkr.

Happy coding!