Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Custom Validation with AngularJS

5.00/5 (3 votes)
29 May 2016CPOL6 min read 37.5K   134  
Create custom validation with AngularJS, including example of comparing two inputs.

Introduction

A description of how to implement custom validation with AngularJS, and how to inform users when they enter an invalid value.

Download source code equals.zip.

Background

While looking at using AngularJS with MVC/WebApi, I noticed that AngularJS does not support HTML5 step attribute.  I started to investigate how to add extra validation to input controls.  The following is the summary of my findings.  Initial searches returned results suggesting implementing a parser function and a formatter function; but after reading AngularJS documentation it became clear that a better method is to implement a validator function.

This article describes how to create and use custom validation in AngularJS (version 1.5.5).  The example will create an Equals validation.  

I will add a link to a second article detailing step validation for number, range, time, date, datetime-local, week and month input controls.

Overview

The example consists of the following parts:

  • HTML page
    • The page must include a script tag to load the angular framework (angular.js).
    • The page must include a script tag to load an angular application module.
    • The page must include a script tag to load the angular validation module.
    • The page optionally includes a script tag to load angular-messages.js, used to display validation messages to the user.
    • The page optionally includes a style tag to load a CSS file with rules for styling input controls with invalid values.
    • HTML elements will include attributes (commonly prefixed with ng or data-ng) to add angular functionality to the element 
  • Angular Application module - a javascript file
    • Its module name will be referenced by an ng-app attribute in the HTML page
    • It will reference angular modules that the application requires
  • Angular Validation module - a javascript file
    • Implementation of custom validation for input controls
  • CSS stylesheet (optional)
    • Style rules to highlight input controls with a value that failed validation

HTML Page using Equals Validator

Details are provided as comments in the source code

HTML
<!DOCTYPE html>
<!-- Add Angular Application (ng-app) attribute to high level element.  The 
     scope of its functionality is limited to this element and its descendants.
     The angular application module is called ngExample -->
<html ng-app="ngExample">
<head>
    <!-- Import CSS stylesheet to highlight input controls with invalid values  -->
    <link rel="stylesheet" type="text/css" href="equals.css"/>
    <title>Equals</title>
</head>
<body>

    <!-- Include novalidate attribute to disable browsers in-built validation 
         as angular validation will be used instead -->
    <form name="formName" id="formId" novalidate>
    
        <label for="input1Id">Input 1</label>
        <!-- Bind input text control #input1Id to input1 model -->
        <input name="input1Name" id="input1Id" type="text" ng-model="input1">
        <!-- Bind input radion control #source1Id to source model, and initial set source to 1 -->
        <input name="sourceName" id="source1Id" type="radio" value="1" ng-model="source" ng-init="source = 1">
        <br/>

        <label for="input2Id">Input 2</label>
        <!-- Bind input text control #input1Id to input1 model -->
        <input name="input2Name" id="input2Id" type="text" ng-model="input2">
        <!-- Bind input radio control #source2Id to source model -->
        <input name="sourceName" id="source2Id" type="radio" value="2" ng-model="source">
        <br/>

        <label for="confirmId">Confirm</label>
        <!-- Bind input text control #confirmId to confirm model -->
        <!-- Include new ng-equals attribute it is set to an expression, 
             the confirm model must match the equals expression to be valid -->
        <!-- The expression could be a simple constant value, or more complex
             like below where confirm must match the input text control selected
             by the sourceName input radio control -->
        <input name="confirmName" id="confirmId" type="text" ng-model="confirm" ng-equals="{{formName['input' + source + 'Name'].$viewValue}}">
        <!-- Angular messages link to input controls $error object -->
        <div ng-messages="formName.confirmName.$error" style="display:inline;">
            <!-- Angular message displayed if the $error object has an equals
                 property, i.e. the control failed equals validation -->
            <span ng-message="equals">Confirm does not match Input {{source}}</span>
        </div>

    </form>

    <!-- Import angular javascript files -->
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.js"></script>
    <script src="angular-validation-equals.js"></script>
    <script src="angular-application-example.js"></script>

</body>
</html>

Angular Application Module (angular-application-example.js)

The name of the application module is ngExample.  The html element in the HTML page includes the ng-app attribute that is set to ngExample.  This simple application is dependent on the ngValidationEquals module (see next section) and on the ngMessages module (will be used to display validation messages). 

JavaScript
(function(window, angular) {
    'use strict';
    
    angular
        .module('ngExample', 
                    ['ngValidationEquals', 'ngMessages']);
})(window, window.angular);

Angular Validation Module (angular-validation-equals.js)

The following module implements a custom validation.  At the very minimum it contains:

  • angular.module('ngValidationEquals', [])
    The first parameter specifies the name of the module.  The second parameter is the list of the module's dependencies, in this case it is an empty list, as this module has no dependencies
  • directive('equals', function () { ... })
    This hooks functionality to the HTML page where equals appears in the page.  The function returns an object which details restrictions, requirements and behavior.
    • restrict: 'A'
      This states that the directive is restricted to attributes.  In this case behavior is attached where an element has an equals attribute (or a data-equals attribute). 
    • require: '?ngModel'
      This lists the controllers that the directive wants access to.  The input control we want to validate should be bound to a model property with ng-model attribute.  To validate the value of the model property we need access to its controller.
      It is possible that an element might have the equals attribute but not the ng-model attribute.  To handle this without resulting in errors, ? is prefixed to ngModel, to say it is optional.  Within the directive's behavior we will check the model controller exists, and if it does not then we will do nothing.
    • link: function (scope, element, attr, ngModelCtrl) { .... })
      The functions will adds behavior where the restrictions and requirements are met.  The fourth parameter holds the controllers, if any, listed by require.  In this example, this function calls a local function called linkEquals.
  • var linkEquals = function (scope, element, attr, ngModelCtrl) { ... }
    The linkEquals local function (1) checks there is a model controller, (2) adds an equals validator to the model controller, and (3) observe changes to the evaluation of the equals expression and force revalidation of the model which will include the equals validator.
    • if (!ngModelCtrl) return;
      A simple check that there is a model controller.  If it does not then there is model to validate.
    • ngModelCtrl.$validators.equals = function (value) { ... }
      This is where the custom validator is added to the model controller.  Here the key name of the new validator is equals, and it is set to a function that takes the user's entered value and returns true if the value is valid and false if the value is invalid.
      Behind the scenes model validation will update classes of the input control and classes of its parent form; and it will update a boolean property of model controller's $error.  The change in class can be used with CSS to change the styling of the input control; and examining $error can be used to display a validation message.
      In this example, either the class ng-valid-equals or ng-invalid-equals will be added to the input control and to the form; and ngModelCtrl.$error.equals will be set to true or false.
    • attr.$observe('equals', function (value) { ... }
      The equals validator is automatically executed when the value of the input control is changed.  But by default angular will not know to rerun the validator when the value specified by the equals attribute has changed.
      To force revalidation we have to observe the equals attribute.  If it changes then the provided anonymous function is called which in turn calls ngModelCtrl.$validate().  This will cause all the model controller's validators to be executed including the equals validator.
JavaScript
(function(window, angular) {
    'use strict';

    var linkEquals = function (scope, element, attr, ngModelCtrl) {

        // Do nothing if there is no ngModel Controller, i.e. no ng-model
        // attribute
        if (!ngModelCtrl) return;

        // If evaluation of the equals expression changes then force
        // revalidation, including the equals validator
        attr.$observe('equals', function (value) {
            ngModelCtrl.$validate();
        });

        // When input's value changed check if the new value is equal to
        // evaluation of the equals expression
        ngModelCtrl.$validators.equals = function (value) {
            return (ngModelCtrl.$isEmpty(value) && 
                        ngModelCtrl.$isEmpty(attr.equals)) ||
                    (value == attr.equals);
        }
    };

    angular
        .module('ngValidationEquals', [])
        .directive('equals', function () {
            // link to equals attribute, its parent element should also
            // have an ng-model attribute
            return {
                restrict: 'A',
                require: '?ngModel',
                link: function (scope, element, attr, ngModelCtrl) {

                    // do nothing if the element has an ng-equal attribute 
                    // in addition to the equals attributes 
                    if (attr.hasOwnProperty('ngEqualto')) return;

                    linkEquals(scope, element, attr, ngModelCtrl);
                }
            }
        })
        .directive('ngEquals', function () {
            // link to ng-equals attribute, its parent element should also
            // have an ng-model attribute
            return {
                restrict: 'A',
                require: '?ngModel',
                link: function (scope, element, attr, ngModelCtrl) {

                    // If using ng-equals attribute make sure step attribute
                    // is kept in sync with it
                    // Change of equals will force revalidation
                    attr.$observe('ngEquals', function (value) {
                        attr.$set('equals', value);
                    });

                    linkEquals(scope, element, attr, ngModelCtrl);
                }
            }
        });

})(window, window.angular);

The above module supports use of ng-equals attribute as an alternative to the equals attribute.  To provide this support a few lines are added the module:

  • .directive('ngEquals', function () { ... }
    A second directive is added with the key ngEquals instead of equals.
  • if (attr.hasOwnProperty('ngEqualto')) return;
    The equals directive includes a line to do nothing if the element also include the ng-equals attribute, this is to prevent the equals validator from being added twice to the element, in such cases the equals validator is added via the ngEquals directive.
  • attr.$observe('ngEquals', function (value) { ... }
    The ngEquals directive includes an instruction to update the value of the equals attribute when the value of the ng-equals attribute changes.  This is important as the equals validator only examines the value of the equals attribute.

CSS (equals.css)

After validation the input control and its parent form will include the class ng-valid-equals or ng-invalid-equals.  The CSS rule below targets the input control with the class ng-invalid-equals, and sets its background color to red.

CSS
/* Match input control with class ng-invalid-equals */
input.ng-invalid-equals {
    background-color: red;
}

Points of Interest

The expression of the equals attribute can range from a simple constant (e.g. equals="hello") to very complex expressions (e.g. equals="{{formName['input' + source + 'Name'].$viewValue}}").  

There can be an issue if the equals expression references a model property.  If the referenced model property is itself invalid then the equals expression will be evaluated to an empty value.

For example if you have a password input control with a minlength of 8 and a confirm password input control with an equals attribute set to {{password}}; the equals validator will return false if password is between 1 and 7 characters long, even when confirm password is identical.

The workaround is to either (1) allow the password model property to be set to invalid values by adding ng-model-options="{allowInvalid:true}" to the password input control; or (2) change the equals expression to reference the $viewValue of the password input control instead of the password property of the model, e.g. equals="{{formName.password.$viewValue}}".

History

  • 2016-05-29: First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)