Introduction
During implementing an Angular project i found Anguler provides many constraint methods, this tip discuss several of them.
Background
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:
<code><form name="bookInfo" novalidate="novalidate" ng-submit="createBook()">
...
</form>
</code>
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:
<code>1. <form name="bookInfo">...</form>
2. <ng-form name="bookInfo">...</ng-form>
</code>
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" ng-required="true" />
- 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 of bookInfo.isbn.$error.required
will be true.
- 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 of bookInfo.isbn.$error.pattern
will be true.
- 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 of bookInfo.title.$error.maxlength
will be true.
- Range
- Constraint:
<input type="number" name="year" ng-model="book.year" ng-min="1459" ng-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.
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:
<code><form name="bookInfo" ng-submit="createBook()" novalidate="novalidate">
...
<input type="submit" ng-disabled="bookInfo.$invalid" />
</form>
</code>
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.
Another 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:
<code><input type="number" name="year" ng-model="book.year" year-period="year-period" ng-required="true" />
</code>
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:
<code>app.directive('yearPeriod', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attribute, ngModel) {
ngModel.$validators.yearPeriod = function(modelValue, viewValue) {
var minYear = 1459;
var maxYear = new Date().getFullYear();
if (modelValue < 1459 || modelValue > maxYear) {
return false;
} else {
return true;
}
}
}
};
});
</code>
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:
<code><input type="text" name="isbn" ng-model="book.isbn" ng-pattern="/\b\d{9}(\d|X)\b/" unique-isbn="unique-isbn" ng-required="true" />
</code>
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.
<code>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();
$timeout(function() {
$http({
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){
if (data.results.length === 0) {
defer.resolve();
} else {
defer.reject();
}
})
.error(function(data, status, headers, config){
console.log("something went wrong...");
});
}, 2000);
return defer.promise;
}
}
};
}]);
</code>