Sometimes client-side validation needs information from a datastore to determine whether input is valid. For instance, you may want to check if a username is already taken. To do this in an Angular application, we can use a custom directive along with the $http
service to validate user input on the fly.
To get started, create a directive, naming it something that will allow you to easily identify its use in HTML markup. We’ll wrap the directive in an immediately-invoked function expression (IIFE) to make sure we don’t accidentally leak any variables into the global scope.
(function () {
var directiveId = 'validatorDuplicateUsername';
app.directive(directiveId, ['$http', '$q',
function validatorDuplicateUsername($http, $q) {
return {};
}]);
})();
Inside the directive object, we’ll use the link
function so we have access to the DOM element on which the directive is applied. This will allow us to optionally pass a value we want to ignore, such as the current username. This is handy if we’re using this directive on an edit view.
Our validator will be part of Angular’s $asyncValidators
collection. We’ll use the $q
promise service so we can manage the pending state during the HTTP request. $q
will also let us short-circuit validation in certain cases.
Angular’s documentation explains $asyncValidators
:
Please note that if $http
is used then it is important that the server returns a success HTTP response code in order to fulfill the validation and a status level of 4xx in order to reject the validation.
(function () {
var directiveId = 'validatorDuplicateUsername';
app.directive(directiveId, ['$http', '$q',
function validatorDuplicateUsername($http, $q) {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
ctrl.\$asyncValidators[directiveId] =
function validate(modelValue, viewValue) {
var ignoreName = attrs[directiveId];
if (ctrl.$isEmpty(viewValue)) {
// Consider empty models to be valid.
return $q.when();
}
if (ignoreName === viewValue) {
return $q.when();
}
var def = $q.defer();
$http.get('/api/users/name/' + value)
.then(success);
return def.promise;
function success(response) {
if (response === undefined) {
def.resolve();
} else {
def.reject();
}
}
};
}
};
}]);
})();
For maintainability, it’s a good idea to add unit tests for this type of directive. If you use Karma, just drop in the unit test below. This unit test also demonstrates how to use the validator in your HTML.
describe('validatatorDuplicateTemplateName', function () {
var $httpBackend,
$scope,
form,
username = 'chris';
beforeEach(module('app'));
beforeEach(inject(function(_\$httpBackend_, $compile, $rootScope) {
$httpBackend = \_$httpBackend\_;
$scope = $rootScope.\$new();
var element = angular.element(
'<form name="myForm">' +
'<input type="text" ng-model="model.someText" ' +
'name="someText" validator-duplicate-username />' +
'</form>'
);
\$scope.model = {
someText: undefined
};
$compile(element)($scope);
form = \$scope.myForm;
}));
it('Should pass with a unique username.', function() {
\$httpBackend.expectGET(
'/api/user/name/' +
username)
.respond(null);
form.someText.$setViewValue(username);
$httpBackend.flush();
expect($scope.model.someText).toEqual(username);
expect(form.someText.$valid).toBe(true);
});
it('Should fail with a duplicate username.', function() {
\$httpBackend.expectGET(
'/api/user/name/' +
username)
.respond({
id: '2fcc7a77-984d-4966-8dce-bf7bd4a3f271',
name: 'chris'
});
form.someText.$setViewValue(username);
$httpBackend.flush();
expect($scope.model.someText).toEqual(undefined);
expect(form.someText.$valid).toBe(false);
});
it('Should pass by ignoring the current username.', function() {
var formWithCurrentUsername = angular.element(
'<form name="myForm">' +
'<input type="text" ng-model="model.someText" ' +
'name="someText" validator-duplicate-username="chris" />' +
'</form>'
);
form.someText.\$setViewValue(username);
expect($scope.model.someText).toEqual(username);
expect(form.someText.$valid).toBe(true);
});
});