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
<!DOCTYPE html>
<!--
<html ng-app="ngExample">
<head>
<!--
<link rel="stylesheet" type="text/css" href="equals.css"/>
<title>Equals</title>
</head>
<body>
<!--
<form name="formName" id="formId" novalidate>
<label for="input1Id">Input 1</label>
<!--
<input name="input1Name" id="input1Id" type="text" ng-model="input1">
<!--
<input name="sourceName" id="source1Id" type="radio" value="1" ng-model="source" ng-init="source = 1">
<br/>
<label for="input2Id">Input 2</label>
<!--
<input name="input2Name" id="input2Id" type="text" ng-model="input2">
<!--
<input name="sourceName" id="source2Id" type="radio" value="2" ng-model="source">
<br/>
<label for="confirmId">Confirm</label>
<!--
<!--
<!--
<input name="confirmName" id="confirmId" type="text" ng-model="confirm" ng-equals="{{formName['input' + source + 'Name'].$viewValue}}">
<!--
<div ng-messages="formName.confirmName.$error" style="display:inline;">
<!--
<span ng-message="equals">Confirm does not match Input {{source}}</span>
</div>
</form>
<!--
<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).
(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.
(function(window, angular) {
'use strict';
var linkEquals = function (scope, element, attr, ngModelCtrl) {
if (!ngModelCtrl) return;
attr.$observe('equals', function (value) {
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.equals = function (value) {
return (ngModelCtrl.$isEmpty(value) &&
ngModelCtrl.$isEmpty(attr.equals)) ||
(value == attr.equals);
}
};
angular
.module('ngValidationEquals', [])
.directive('equals', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attr, ngModelCtrl) {
if (attr.hasOwnProperty('ngEqualto')) return;
linkEquals(scope, element, attr, ngModelCtrl);
}
}
})
.directive('ngEquals', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attr, ngModelCtrl) {
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.
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