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 ofbookInfo.isbn.$error.required
will be true.
- Constraint:
- 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 ofbookInfo.isbn.$error.pattern
will be true.
- Constraint:
- 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 ofbookInfo.title.$error.maxlength
will be true.
- Constraint:
- 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.
- Constraint:
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 eachngModel
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.
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.
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 theng-messages-multiple
on the directive containter. And by usingng-messages-include
the specified template can be included into the ng-messages container. -
ngMessage
: has the purpose to show and hide a particular message. ForngMessage
to operate, a parentngMessages
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 thatneMessages
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
.