How to create custom validator directives with AngularJS

How to create custom validator directives with AngularJS
Reading Time: 9 minutes

This article we will show you how to use AngularJS 1.3.x features (or later) to create a form with special requirements like the one for creating a new account on a server. Furthermore, ngMessages will be used to properly display any error messages.
As you may have already guessed, a minimal account creation form requires at least two or three fields: a username, a password, and a password match field. While the user is typing in the username field, it would be nice if the field validation would keep in touch with the server and interactively check if it’s current value already exists or not and inform the user about this. On the other hand, this checking should not be too often as this may “stress” the user.
The create account request should only be made if all the fields of the form are valid.
This article assumes that you are already familiar with Angular’s FormController and you are aware of the ngModelController‘s existenceWe love sharing our Angular knowledge with our readers, so feel free to read our articles on using Angular for interactive charts and integrating animate.css.

Brief description about how validators work


Angular instantiates a FormController for each HTML form tag. This form controller is private by default and it can not be referred from any outer controller unless the programmer specifies a form name using name attribute as on the form element.
Each input field with a ng-model attribute on it has a ngModelController associated with it. This controller watches for data changes on the input it is associated with. On data change detection it runs a series of parsers, formatters and validators.
A validator works on the information of the input field which is associated with, by checking the field value against a certain restriction.
Each ngModelController contains a list of registered validators, which are stored in a hashtable style in $validators and  $asyncValidators objects.

About custom validators


Angular allows the creation of custom element directives so that programmers can significantly increase the power of a simple form. For more information on validation, you can read our article on number overflow in Javascript.
An Angular custom validator requires to get a hold on the ngModelController of an input field to work with.  This is accomplished by using require: ‘ngModel’ in the directive return object, as in the examples above. Also, in order for a validator to be useful we must register it in one of the $validators or $asyncValidators objects.
When an input field changes its value (on certain conditions and timing) all the validators attached to it will be run against the new value. If one or more validations fail they will individually determine the creation of an entry in the $error object associated with the respective input. We will use that to detect which error message to display and when to display it.
A custom validator directive structure would typically look like this:

angular
  .module('app')
  .directive('customValidatorName', customValidatorDirectiveFunc);
function customValidatorDirectiveFunc() {
   return {
     restrict: 'A',
     require: 'ngModel',
     link: function (scope, element, attr, ngModel) {
        // validation callback registration to ngModel
     }
   }
}

 

Create a basic HTML form template


Let’s create a named form and two input fields: username, password.

<form name="newAccountForm">
    <div>
      <label>Username:</label>
      <input type="text"
             name="usernameField"
             placeholder="username here"
             ng-model="username"/>
    </div>
    <div>
      <label>Password:</label>
      <input type="password"
             name="passwordField"
             ng-model="password"
             placeholder="password here"/>
    </div>
</form>

Briefly, this form template will generate a logical structure similar to this one:

newAccountForm = {
    usernameField: {
      $validators: {},
      $asyncValidators: {},
      $error: []
    },
    password: {
      $validators: {},
      $asyncValidators: {},
      $error: []
    }
  }

Let’s create the username validator first.

Create the username availability validator


Outer look:

angular
  .module('app')
  .directive('checkAvailability', checkAvailabilityFunc);
  function checkAvailabilityFunc($http, $q) {
    return {
      restrict: 'A',
      require: 'ngModel',
      link: function(scope, element, attr, ngModel) {
        // fetch the call address from directives 'checkIfAvailable' attribute
        var serverAddr = attr.checkAvailability;
        ngModel.$asyncValidators.invalidUsername = function(modelValue, viewValue) {
          // validation logic here
        }
      }
    }
  }

We want this validator to be treated as an attribute,  so we add restrict: ‘A’ to the return object.
Next, we request the ngModelController adding require: ‘ngModel’.
In the link function parameters list we add ngModel (or ngModelCtrl as others prefer) which represents the ngModelController instance attached to the input element we wish to validate.
The next step is to register the validation callback in $asyncValidators. This is achieved easily, by adding a new property to an object.

ngModel.$asyncValidators.invalidUsername = function(...) {...}

Maybe a question arises here:
Why $asyncValidators and not $validators?
The answer is because we will be doing a server http request which we don’t know how long will last until completion, so instead of freezing the user input until we receive the server response we use this validator category and behaviour offered by Angular.
I also added a server address to the validators attribute which I can read using attr.<directive name> like this: attr.invalidUsername.
Now let’s fill in the validation logic.

ngModel.$asyncValidators.invalidUsername = function(modelValue, viewValue) {
  var username = viewValue;
  var deferred = $q.defer();
  // ask the server if this username exists
  $http.get(serverAddr, {data: username}).then(
    function(response) {
      if (response.data.exists) {
        deferred.reject();
      } else {
        deferred.resolve();
      }
    });
  // return the promise of the asynchronous validator
  return deferred.promise;
}

Inside the validator function we make the server call. Here I am making the call using $http but this is not mandatory. The serverAddr is the same as mentioned before.
Thing is, this validator must return a promise which will be rejected or resolved by us. You can see I wrapped the server response in another promise which I returned to Angular.
Angular doesn’t care if/what values you pass to your resolve()/reject() call when you decide if a value is valid or not. This aspect will not decide if the validation is passed or not. The decisive thing is only the final status of the promise, which is simply resolved or rejected.
If the promise is rejected, it means that the validation failed and an invalidUsername error property is created in the $error field of the associated input element. If the promise is resolved it means the validation passed and any invalidUsername error will be removed, if any was present until now.
We can’t just return the $http request promise to Angular because Angular simply doesn’t care what the server says… We must do this by our own, so we listen to it’s response and check the response status/message, then decide if the validation has failed or not.

Here is the full directive code, with server call mocked:

angular
.module('app')
.directive('checkAvailability', checkAvailabilityFunc)
.run(serverMock);
function checkAvailabilityFunc($http, $q, $timeout) {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, element, attr, ngModel) {
      // fetch the call address from directives 'checkIfAvailable' attribute
      var serverAddr = attr.checkAvailability;
      ngModel.$asyncValidators.invalidUsername = function(modelValue, viewValue) {
        var username = viewValue;
        var deferred = $q.defer();
        // ask the server if this username exists
        $http.get(serverAddr, {data: username}).then(
          function(response) {
            // simulate a server response delay of half a second
            $timeout(function() {
              if (response.data.exists) {
                deferred.reject();
              } else {
                deferred.resolve();
              }
            }, 500);
          });
        // return the promise of the asynchronous validator
        return deferred.promise;
      }
    }
  }
}
function serverMock($httpBackend) {
  var occupiedUsernames = ["Joe", "Joee", "Rogers", "Kim"];
  // mock the server GET username availability request
  $httpBackend.whenGET("localhost:3000/users/").respond(
    function(method, url, data) {
      var username = data;
      var exists = occupiedUsernames.indexOf(username) > -1; // 'true' if username is in array
      return [200, {exists: exists}, {}];
    }
  );
}

The server mock implementation is not the subject of this demo.
viewValue or modelValue?
Because our input element is of type=”text”, it doesn’t matter.. But if it were a type=”number” input, the difference between viewValue and modelValue would be in the fact that viewValue would see the number as a string of numeric chars while modelValue would see the number string converted into a primitive number.

Enhance username input element


<div>
  <label>Username:</label>
  <input type="text"
         name="usernameField"
         placeholder="username here"
         ng-model="credentials.username"
         minlength="3"
         ng-model-options="{updateOn: 'default blur', debounce: {default: 500, blur: 0}}"
         check-availability="localhost:3000/users/"/>
  <span ng-messages="newAccountForm.usernameField.$error" ng-show="newAccountForm.usernameField.$dirty">
    <span ng-message="required">Required username!</span>
    <span ng-message="minlength">Too short!</span>
    <span ng-message="invalidUsername">Username not available!</span>
  </span>
</div>

Notice the presence of check-availability attribute with “localhost:3000/users/” value, which corresponds to serverAddr in the validator directive. I also added a built-in validator called minlength, which will trigger a “minlength” error when the username is shorter than 3 characters.
There is also this element: ng-model-optionwhich says: Update the ng-model value once every 500ms when the focus is in the input element, or update instantly when the input focus is lost.
Next you can see a usage example of ngMessages and ngMessage directives, which are also very simple to use. ngMessages has child ngMessage tags which behave similarly to ngShow when an error name is present in the $error array. The difference is that only one ngMessage tag is active at a time (in the default mode) and the priority is from top to bottom.
Note that we only show the error messages when the user has modified something in the input field (i.e. when $dirty is true).
The final body template will look like this:

<body ng-controller="AccountCreationCtrl">
  <h3>New account form:</h3>
  <form name="newAccountForm" class="css-form" ng-submit="createNewAccount(credentials)">
    <div>
      <label>Username:</label>
      <input type="text"
             name="usernameField"
             placeholder="username here"
             ng-model="credentials.username"
             minlength="3"
             ng-model-options="{updateOn: 'default blur', debounce: {default: 500, blur: 0}}"
             check-availability="localhost:3000/users/"/>
      <span ng-messages="newAccountForm.usernameField.$error"
            ng-show="newAccountForm.usernameField.$dirty">
        <span ng-message="required">Required username!</span>
        <span ng-message="minlength">Too short!</span>
        <span ng-message="invalidUsername">Username not available!</span>
      </span>
    </div>
    <div>
      <label>Password:</label>
      <input type="password"
             name="passwordField"
             placeholder="password here"
             ng-model="credentials.password"
             minlength="3"/>
       <span ng-messages="newAccountForm.passwordField.$error"
             ng-show="newAccountForm.passwordField.$dirty">
         <span ng-message="required">Password is required!</span>
         <span ng-message="minlength">Too short! (at least 3 chars)</span>
       </span>
    </div>
    <div>
      <input type="submit"/>
    </div>
  </form>
</body>

Here is a plunker link to help you save time. I hope you enjoyed this demo. Keep up the good work and keep up the good code!

We transform challenges into digital experiences

Get in touch to let us know what you’re looking for. Our policy includes 14 days risk-free!

Free project consultation