Recently we’ve been redesigning one of our web applications. The app includes several forms which we’ve decided to validate on the client-side using AngularJS form validation.

Introduction

Recently we’ve been redesigning one of our web applications. The app includes several forms which we’ve decided to validate on the client-side using AngularJS form validation. During the implementation we’ve solved several interesting problems not covered by the official AngularJS documentation. In this blog entry the solutions we came up with are shared. To understand this post you should be familiar with the basic angular concepts like controllers, scope and directives.

Form Validation Best Practices

Discussing web form usability is out of scope for this blog post. However to give you some context, solutions, proposed in this article, are based around the idea of inline validation. It means that errors are displayed alongside the inputs. An input is validated and errors are displayed after a user is done editing and moves to the next field. Technically speaking, the validation happens after losing focus (onblur). If the user returns to edit an invalid field, all errors are cleared. This way the user is not distracted while entering information and gets feedback right after he’s done.

Another conscious decision we’ve made was to keep the save button active, even if the form is invalid. This was done to support onblur validation. The save button can’t be disabled because the user might try saving without tabbing out of an invalid field (errors are hidden while the input is in focus). To work around this issue, clicking the Save button forces all error messages to be displayed.

We’ve also organized our forms into cards. A card is just a container that can be expanded or collapsed by clicking the header. Each card has a Save and Cancel button. After expanding a card and editing its contents you can't collapse it or expand another card until you click Save or Cancel. This prevents users from forgetting about unsaved changes. Furthermore, cards keep the users focused by naming and separating tasks. Below there’s a screenshot of four cards. The second one is expanded.

If you would like to learn more there’s a great article on inline form validation.

Comparing Two Inputs

Sometimes you might want to mark the form valid only if two separate inputs are equal / unequal. For example, when creating a new account, you might want the user to enter important information like e-mail or password twice to minimize the chance of a typo. Angular doesn’t have a solution for comparing two fields out of the box, so we’ve written a handy custom directive. In the following HTML snippet it’s applied to the second password input:

The value of the first password input is passed into the directive through the adf-equals attribute. According to the best practices we prefix all our custom directives with adf which stands for Adform. Let’s examine the JavaScript code that registers the directive. First of all, you may have noticed that the directive is declared in an anonymous function like so ( function () { /* directive */ } ) ();. This is called the Immediately Invoked Function Expression pattern.

Angular validates a form if all registered validators are valid. There's no built-in validator for comparing two fields, so we register a new one $validator.equals. The registered validator compares the second password input (viewValue) with the first password input (scope.compareTo). As you may have guessed, Angular takes the value of the adf-equals HTML attribute and puts it into scope.compareTo.

Invalidating a Valid Form after Submission

It makes sense to validate a form before submitting it to a server. It’s the default workflow in Angular and is easy to implement. Even if a field can’t be validated by the client, Angular provides asynchronous validators that query the server before making a decision. Async validation is also done without submitting the whole form. Of course, all the data must be re-validated on the server because the client-side validation can be easily bypassed and is used primarily for a better user experience.

We’ve found out that some valid forms have to be invalidated after they have been submitted to the server. For example, the form for changing the account e-mail requires the user to provide a valid password. This is done to prevent the account hijacking. Using the async validator to check the password after the user stops typing or the password input loses focus is a bad idea. Exposing the password validation service to the client without rate limiting would allow for password brute-forcing. So what if we need to invalidate a form after it’s been marked valid and submitted? It turns out, Angular doesn’t support this kind of scenario by default. Luckily, we’ve found a solution and are going to share it.

First, let’s consider requirements of server implementation. The server must separate the form validation errors from the server errors because the client will handle them differently. To identify the server errors we’re using HTTP status code 500 “Internal Server Error”. In case of a form validation error we set the response status to 400 “Bad Request” and include a list of error messages in JSON format. You can see the response schema below.

On the client-side, after detecting a server error, the app UI is blocked and a global error message is displayed. In case of a form validation error, the error messages are displayed under the related input fields. Here’s a simplified excerpt from a controller that handles the post-submit form validation:

Firstly, we only allow valid forms to be saved to the server because it might contain fields that can be validated before submitting. After calling the service we wait for the response and, if it contains the form validation errors, we register a new invalid validator called “backend”, then add the error messages to the $scope. You can use ng-repeat to iterate over the errors in HTML. In the example below the outer div controls the visibility of the error list. The inner div lists the error massages.