Menu

AngularJS AJAX Validation

December 6, 2016 by Christopher Sherman

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:

  • A collection of validations that are expected to perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided is expected to return a promise when it is run during the model validation process. Once the promise is delivered then the validation status will be set to true when fulfilled and false when rejected. When the asynchronous validators are triggered, each of the validators will run in parallel and the model value will only be updated once all validators have been fulfilled. As long as an asynchronous validator is unfulfilled, its key will be added to the controllers $pending property. Also, all asynchronous validators will only run once all synchronous validators have passed.

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);
});
});

AngularJS JavaScript