AngularJS Tutorial - 7

New version here

Note that the all tests are present inside the sample, but we will see them at the end of this chapter, when all hopefully will be clear.

Adding validation

First there are various attributes that can be added to the input fields to validate.

This can be done through attributes:

Or adding the following attributes with the following values to the input:

Now a verification for the validity can be added to the form. We are taking the add.html as an example now.

<button class="btn btn-primary" id="add-new-btn" 
        ng-disabled="addNewForm.$invalid || (isUnchanged(contact) && addNewForm.$invalid)" 
        ng-click="save(contact)">Save!</button>

The first directive: validation

Directives, in AngularJS are extensions to the html syntax. We have found many of such directives previously in this tutorial, like "ng-view", "ng-app". We could add our own directives to simplify our job. One situation in wich they should be used is for validation purposes. Now we will add a directive to allow only phone numbers starting with "+", with international prefix. This could be achieved through a regular expression, but this is mostly to give an idea of the validation.

Testing (a directive)

Generally i consider it an hard task, but let's start. First we should declare the html that will be used as a template to apply the directive. Of course the directive must be present on this html.

    describe("phoneNumber", function() {
        var html = '<form novalidate name="testForm" id="test-form" method="post" action="">';
        html += '<input type="text" name="phone" ng-model="phone" phone-number/>';
        html += '</form>';

Then we should define

           
    var scope = null;
    var elem = null;
    var compiled = null;
    
    beforeEach(function (){

We call the function inject passing the parameters that will be injected. Those will be used to initialize the test. A new scope just for our test is created, and a DOM element is generated through jqLite (the partial Angular implementation of JQuery.

Then the DOM generated is compiled, and the scope is applied.

Lastly the scope.$digest is called. This is needed to instantiate all the angular stuffs inside the scope and DOM.

       
        inject(function ($compile, $rootScope) {
            //create a scope (you could just use $rootScope, I suppose)
            scope = $rootScope.$new();
            //get the jqLite or jQuery element
            elem = angular.element(html);
            //compile the element into a function to process the view.
            compiled = $compile(elem);
            //run the compiled view.
            compiled(scope);
            //call digest on the scope!
            scope.$digest();
        });
    });

Now the test. We set the variable on the scope, we call $digest to apply all changes, than we search on the dom for the element the we are checking, and we verify the conditions.!

       
    it('should add invalid class when invalid', function () {
        scope.phone = "aaa";
        scope.$digest();
        var cssClass = elem.find("input");
        expect(cssClass.length).toBe(1);
        expect(cssClass.hasClass("ng-invalid")).toBe(true);
        expect(cssClass.hasClass("ng-invalid-phone-number")).toBe(true);
    });

Implementation

We now add a new js, "app/services.js" and include it into the index.html the service will provide the validation functions for the directive. The function will take as input the attributes, the form controller and the value to validate.

app.factory('phoneNumberValidatorService',[
    function() {
        var phoneRegExp = new RegExp(/^[\d|\+|\(]+[\)|\d|\s|-]*[\d]$/);
        return function( attr, ctrl, value){
            var valid  = true;

First a verification on the mandatory attribute, since we choose that an empty phone number will not be valid when mandatory. Then we call the ctrl.$setValidity. This function will set the form as valid or invalid, and set the value $valid to true on the input text. This way using formName.inputName.$valid we can check for the field validity. Then a boolean valid is returned.

    if(value==undefined || value.length==0){
        if(attr.mandatory!==undefined){
            valid = false;
            ctrl.$setValidity('required', false);
        }
        ctrl.$setValidity('phoneNumber', true);
    }else{
        if(attr.mandatory!==undefined){
            ctrl.$setValidity('required', true);
        }
        if(phoneRegExp.test(value)){
            ctrl.$setValidity('phoneNumber', true);
        }else{
            valid = false;
            ctrl.$setValidity('phoneNumber', false);
        }
    }
    return valid;
    ...

Now we can setup the real directive, that depends on the phoneNumberValidatorService

app.directive('phoneNumber',['phoneNumberValidatorService', 
    function(phoneNumberValidatorService) {
        return {

The restrict tells where the directive can be applied, for example

    // restrict to an attribute type.
    restrict: 'A',

Define (optionally) if some particolar controller is required. The element must have ng-model attribute.

    require: 'ngModel',

Define the link function. This is executed in alternative to the compile function (will see later how it works).

           
    link: function(scope, elem, attr, ctrl) {

Add a parser that will process each time the value is parsed into the model when the user updates it. If it's valid, return the value to the model, otherwise return undefined.

   
    ctrl.$parsers.unshift(function(value,$scope) {
        var valid = phoneNumberValidatorService( attr,ctrl,value);
        return valid ? value : undefined;
    });

Add a formatter that will process each time the value is updated on the DOM element. This simply returns the value.

   
    ctrl.$formatters.unshift(function(value) {
        phoneNumberValidatorService( attr,ctrl, value);
        return value;
    ...

Now we can add the validation to the phone input!

    <label for="phone">Phone:</label>
    <input type="text" name="phone"
        ng-model="contact.phone"
        phone-number mandatory/>

Screenshot

The second directive: show errors by field

The idea is to write something like the following. To show specific error descriptions on the form when the input will not be valid.

    <label for="phone">Phone:</label><input type="text" name="phone"
        ng-model="contact.phone" phone-number mandatory/>
    <show-error for="phone"></show-error>

TranslatorService

First we should create a service that translates the parameters passed to setValidity into something more human readable. The errorTranslatorService.

The testing is left to the reader as an exercise. Anyway it's pretty simple, just remember to create the right parameter format to pass to the translator.

   
app.factory('errorTranslatorService',[
    function() {

Note that we are creating the service object with functions, like we did with the repository.

       
        var errorTranslatorService = {
            translate:function(element_error){
                var result = new Array();

The structure of the errors for Angular is like the following. If the values are true it means that an error is set for that specific validator!

   
{
    $error: {
        phoneError: true,
        email: false,
        mandatory: true
    }
}

Coming back to our validator, we seek on the possible errors.

                for (var errorKey in element_error.$error){
                    var error = element_error.$error[errorKey];
                    if(this.translators[errorKey]!==undefined && error){
                        var errorResult = this.translators[errorKey](error);
                        if(errorResult!=null) result.push(errorResult);
                    }
                }
                return result;
            },
            register:function(errorName,translationCallback){
                this.translators[errorName]=translationCallback;
            }
        }

The variable is initialize, since whe should cope with the oddities of class declarations in Javascript

        errorTranslatorService.translators = new Array();

Then we register all the errors present on AngularJS

           
        ...
        errorTranslatorService.register("email",function(error){ 
            return error?"Not a valid   e-mail":null;});

        return errorTranslatorService;
    }

ShowError, testing

We will follow the pattern we've already seen for the phoneNumber directive. The html will change, sine we already know that we will need to be in a form.

var html = '<form novalidate name="testForm" id="test-form" method="post" action="">';
html += '<input type="email" name="phone" ng-model="phone" mandatory/>';
html += '<show-error for="phone"></show-error>';
html += '</form>';

Then we could write the tests

    it('should render the template with error when invalid value', function () {
        scope.phone = "notAnEmail";

The two following commands are needed to set the scope.phone variable as dirty. This is needed to allow the $digest to process correctly the directive and to do all the elaborations to modify the dom.

        scope.testForm.phone.$dirty=true;
        scope.testForm.phone.$pristine =false;
        scope.$digest();
        var errorItem = elem.find("span");
        expect(errorItem.length).toBe(1);
        expect(errorItem.attr("ng-show")).toContain("testForm.phone.$dirty");
        expect(errorItem.attr("ng-show")).toContain("testForm.phone.$invalid");
        expect(errorItem.text()).toContain("Not a valid e-mail");
    });

The other tests can be found on the sample.

ShowError, implementation

... here comes troubles...

We need first the dependency on the translator service

app.directive('showError', ['errorTranslatorService',
    function(errorTranslatorService) {

A default template is defined to show error (a simple red string). Note that we need to acces the binding to the variable inside the expressions. So we will add some replacement! Note that these variables are Javascript variables. They are NOT on the AngularJS scope. Instead, the showErrorString() that is shown with the double curly braces is relative to the scope of the template.

        var templateText = '<span style="color:red" '+
            'ng-show="#FORM#.#MODEL#.$dirty && #FORM#.#MODEL#.$invalid">'+
            '{{showErrorString()}}</span>';

This function will serve to replace the item showError, with the template content.

        var applyTemplate = function(item,elem, attr,newText){
            var forElement = attr.for;
            var el = angular.element(newText);
            elem.replaceWith(el);
        };
        
        return {
            restrict: 'E',

This is needed to create a child scope, to avoid interact with the parent scope and having a kind of "clean" environment.

           
            scope:true,
            require: '^form',

The scope is missing. The compile will be executed once for each directive instance, before the scope initialization. The scope stuffs will be made inside the directive controller, later.

            compile: function(elem, attr, form) {
                var forElement = attr.for;
                var parent = elem;
                var formName = "";

Seeck for the form name

                while(parent[0].tagName.toUpperCase()!="FORM"){
                    parent = parent.parent();
                    formName = parent.attr("name");
                }

Setup the model and form on the template text.

                var newText = templateText.replace(/#MODEL#/g,forElement);
                newText = newText.replace(/#FORM#/g,formName);
                applyTemplate(this,elem,attr,newText);
            },

Then the controller that will be used to initialize the scope. Two functions are added to the scope, one to show a string with all the errors, and the other to return an array of errors

            controller: function($scope, $element, $attrs) {
                var forElement = $attrs.for;
                var form = $element.controller("form");
                $scope.showErrorString = function(){
                    if(!form[forElement].$valid && form[forElement].$dirty){
                        var foundErrors = errorTranslatorService.translate(form[forElement]);
                        return "Errors :"+foundErrors.join(", ");
                    }
                    return "";
                };
                $scope.showError = function(){
                    if(!form[forElement].$valid && form[forElement].$dirty){
                        var foundErrors = errorTranslatorService.translate(form[forElement]);
                        return foundErrors;
                    }
                    return new Array();
                }
            }
        };
    }
]);

Now we could think about using a template file like add.html and list.html. We will use "assets/app/error.html"

Pre-loading a template into the cache

We should load this template once for all, since it will be used nearly everywhere. First we should define a constant where it's defined. We create a separate constants.js to isolate all constants (and to override them for testing purposes)

app.constant('templateTextUrl', "assets/app/error.html");

Then whe should load the template into the cache. We will use the "run" initializer, that runs after everithing had been loaded. In it we load the result of the $http.get into the $templateCache. Note that we are passing the "templateTextUri" as an injected parameter, since it's a constant!

We are doing this here, since the $http.get is asynchronous (as everything in Angular) and we could not call directly the $http.get into the showError without messing with the control flow.

app.run(['$templateCache','$http','templateTextUrl',
    function($templateCache,$http,templateTextUrl){
        if(templateTextUrl!==undefined && templateTextUrl.length> 0)
            $http.get(templateTextUrl, {cache:$templateCache});
    }
]);

ShowError using the template cache.

We add the templateCache and the templateTextUrl as dependencies.

app.directive('showError', ['errorTranslatorService','$templateCache','templateTextUrl',
    function(errorTranslatorService,$templateCache,templateTextUrl) {

Then to load the template from the url, first we check if there is an attribute that tells us to avoid loading the utl (use-default).

    var tmpTemplate = templateText;
    if(attr.useDefault===undefined){
        var templateCached = $templateCache.get(templateTextUrl);
        if(templateCached!==undefined) tmpTemplate = templateCached[1];
    }

We then write the template. Note that we use the showError function that returns an array. The function is on the scope thus we can use it as a parameter for the ng-repeat.

<span style="color:red" ng-show="#FORM#.#MODEL#.$dirty && #FORM#.#MODEL#.$invalid">
<ul><li ng-repeat="error in showError()">
    {{error}}
</li></ul>
</span>

For the test we will use, instead of app/constants.js we will use spec/app/app.constants.js, where the templateTextUrl will be set to empty. This way it will NOT be used. This is because i have not found a decent way to initialize it through the mocks, and because i could not use optional parameters inside the declarations of directives, controllers etc.

app.constant('templateTextUrl', "");

Download the sample source


Last modified on: September 16, 2014