Asynchronous Validations in AngularJS

One of the features introduced in Angular 1.3 is Asynchronous Validation. Angular already allows us to define custom validations, but all validations previously had to return inline. The new asynchronous validations allows us to return a promise from our custom validation. I’m going to build an example to see how this works.

The front-end code for this example is available on Plunker and the server-side app is available on Github.

(Note that my examples use CoffeeScript, not JavaScript.)

Creating the validator

The custom validator is implemented as a directive, which requires the ngModel directive. This means our directive can only function when an ngModel directive has been declared on the same element.

app.directive "exUsername", ->
  restrict: "A"
  require: "ngModel"
  link: ($scope, element, attributes, ngModelController) ->

When you specify the require option in your directive Angular will look for that directive on your element and provide the controller instance of the required directive as the 4th parameter in your link function. This is a really powerful and clean way to communicate between directives. You can also require multiple directives, in which case the 4th parameter in your link function becomes an array of controller instances.

I can now use my custom validator together with the ngModel directive on an input directive.

<input type="text" name="username" ng-model="username" ex-username />

Performing the validation

The required ngModelController has an $asyncValidators property where we can add our validation. We attach our validation function to this property. When executed, our function returns a promise - if the promise is rejected, the validation fails - if the promise is resolved, the validation passes.

To test this part of the example I have created a small Sinatra app with a single endpoint.

get "/usernames" do
  query = params["q"]
  capitalizedQuery = query && query.capitalize

  matches = ["Bob", "Steve", "James", "Dave"].select do |username|
    username == capitalizedQuery
  end
  matches.to_json
end

We now need to call this endpoint and check the response. If any matching usernames are returned it means we need to fail the validation by rejecting the promise.

app.directive "exUsername", ($http, $q) ->
  restrict: "A"
  require: "ngModel"
  link: ($scope, element, attributes, ngModelController) ->
    ngModelController.$asyncValidators.usernameAvailable = (username) ->
      $http
        .get("http://localhost:4567/usernames?q=#{username}")
        .then (response) ->
          if response.data.length > 0
            $q.reject("Username has already been taken")
          else
            true

(Note that I am hardcoding the server URL here, in order to be able to test this example from Plunker by hitting my local sinatra server.)

Showing a loading animation

While we querying the server to see if our username is available, we can show a message to the user to indicate that we are performing this validation. In order to accomodate asynchronous validations Angular has introduced a new $pending property.

Both the form and the model will set the $valid and $invalid flags to undefined once one or more asynchronous validations have been triggerred. At the same time the $pending flag will be set on both the form and the model. Once all validations have been completed the $pending flag will be removed and the $valid and $invalid flags will be restored.

We can use this flag to show a message to the user.

<input type="text" name="username" ng-model="username" ex-username />
<span ng-if="exForm.username.$pending">
  Checking Username...
</span>

We can also prevent the user from submitting the form while the validation is pending.

<input type="submit" value="Create User" ng-disabled="exForm.$invalid || exForm.$pending" />

Prevent Excessive Backend Calls

This example works, but if we look at the server logs it looks a bit worrisome. For example, if I type ‘James’ into the input box I will see the following logs:

"GET /usernames?q=J HTTP/1.1" 200 2 0.0006
"GET /usernames?q=Ja HTTP/1.1" 200 2 0.0006
"GET /usernames?q=Jam HTTP/1.1" 200 2 0.0006
"GET /usernames?q=Jame HTTP/1.1" 200 2 0.0006
"GET /usernames?q=James HTTP/1.1" 200 9 0.0006

The validation is being executed for every keystroke - this may or may not be a potential issue in terms of server load, but it is definitely rather inefficient. Luckily, Angular provides us with an easy way of cleaning this up.

Firstly, by default asynchronous validations will only run if all normal validations have passed. For example, if we add a required attribute to our input the asynchronous validation will only run after we have entered at least one character.

Secondly, we can specify debounce options using the ngModelOptions directive. This directive allows us to specify a debounce value - so our asynchronous validation will only be triggerred after input has stopped arriving for a certain threshold limit. We can even specify a default debounce value, but have all validations execute immediately when the user tabs out of the input.

In this example, the validations will only execute after input has stopped arriving for 300 milliseconds, or if the field loses focus (blur).

<input type="text"
       name="username"
       ng-model="username"
       required
       ex-username
       ng-model-options="{ updateOn: 'default blur', debounce: {'default': 300, 'blur': 0} }"
       />

That’s all there is to it! Happy coding.