On the Bleeding Edge: Advanced AngularJS Form Validation

(Note: This article has been updated to use AngularJS 1.0.8 and Bootstrap 3.0.0. If you are using earlier versions the syntax may be slightly different.) We’ve been using AngularJS for the past few months and in general it has been really great. There has definitely been a learning curve, but the best part of the design is that it really encourages you to extend it properly. Whenever you try to hack something, it just feels wrong and you are forced to look at your problem from a different perspective. The benefit is the resulting solution is usually much more modular and portable.

I have had several occasions where I’ve had to write my own directives, providers, filters, etc.

Advanced Form Validation

One of the cool features of angular is the form validation. By decorating your field elements with the correct attributes, angular validation will occur automatically and let you know when a field (and in turn the form) is valid or invalid. In addition you can use some conditional css classes to highlight invalid fields to the user. Below is a simple example of a login form, where we keep the “Login” button (i.e. “call to action”) disabled until the form is valid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   <form name="loginForm" novalidate 
     ng-app="LoginApp" ng-controller="LoginController" ng-submit="login()">
     <div class="form-group">
       <input class="form-control" name="username" type="text" 
         placeholder="Username" required ng-model="session.username" />
       <span class="help-block" 
         ng-show="loginForm.username.$error.required">Required</span>
     </div>
     <div class="form-group">
       <input class="form-control" name="password" type="password" 
         placeholder="Password" required ng-model="session.password" />
       <span class="help-block" 
         ng-show="loginForm.password.$error.required">Required</span>
     </div>
     <div class="form-group">
         <button type="submit" class="btn btn-primary pull-right" 
           value="Login" title="Login" ng-disabled="!loginForm.$valid">
           <span>Login</span>
         </button>
     </div>
   </form>

Result:

The above example is functional and works fine. But in reality, its not the greatest user experience. Disabling a call to action like “Login” without a clear reason why can be confusing to the user, even in this simple example of required fields. So using AngularJS (and Bootstrap for easy styling) you can better highlight the fields that need attention:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   <form name="loginForm" novalidate 
     ng-app="LoginApp" ng-controller="LoginController" ng-submit="login()">
     <div class="form-group"
       ng-class="{'has-error': loginForm.username.$invalid}">
       <input class="form-control" name="username" type="text" 
         placeholder="Username" required ng-model="session.username" />
       <span class="help-block" 
         ng-show="loginForm.username.$error.required">Required</span>
     </div>
     <div class="form-group"
       ng-class="{'has-error': loginForm.password.$invalid}">
       <input class="form-control" name="password" type="password" 
         placeholder="Password" required ng-model="session.password" />
       <span class="help-block" 
         ng-show="loginForm.password.$error.required">Required</span>
     </div>
     <div class="form-group">
         <button type="submit" class="btn btn-primary pull-right" 
           value="Login" title="Login" ng-disabled="!loginForm.$valid">
           <span>Login</span>
         </button>
     </div>
   </form>

Result:

So now our controls are conditionally decorated with an ‘has-error‘ css class if they are currently invalid courtesy of the ‘ng-class‘ directive. But things still aren’t great. We still have a disabled call to action and even though we are highlighting the invalid fields, we are doing so right away. Before the user has done anything we are saying “You’ve done something wrong”. The solution for me was some custom directives. Let’s take a look at the disabled call to action first.

The reason we use the ‘ng-disabled‘ directive to disable the call to action is to prevent the form from being submitted when we know it to be invalid. We could remove the ‘novalidate‘ attribute on the form which will allow the built in browser validation to stop submission based on the ‘required‘ attribute, however there are several other non-standard html5 attributes we could potentially use to validate the field and the built-in browser validation would ignore those. So we basically want to allow for a submission to be attempted via the button click, but cancel it if the form is invalid. Enter ‘rcSubmit‘: (Note: all of our custom directives begin with our own prefix/namespace “rc”, for “RealCrowd”, to avoid any conflicts with other directives we may import)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    var rcSubmitDirective = {
        'rcSubmit': ['$parse', function ($parse) {
            return {
                restrict: 'A',
                require: 'form',
                link: function (scope, formElement, attributes, formController) {
 
                    var fn = $parse(attributes.rcSubmit);
 
                    formElement.bind('submit', function (event) {
                        // if form is not valid cancel it.
                        if (!formController.$valid) return false;
 
                        scope.$apply(function() {
                            fn(scope, {$event:event});
                        });
                    });
                }
            };
        }]
    };

As you can see the directive is pretty simple. It binds to the form’s submit event, and if the ngFormController is not valid, cancels the event. Otherwise it will execute the defined expression. This is basically a copy of the angular ngSubmit with the addition of a validation check. So we just replace the ‘ng-submit‘ attribute with the ‘rc-submit‘ and now we can remove the ‘ng-disabled‘ attribute from our login button because now the submit will not execute if the form is invalid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   <form name="loginForm" novalidate 
     ng-app="LoginApp" ng-controller="LoginController" rc-submit="login()">
     <div class="form-group"
       ng-class="{'has-error': loginForm.username.$invalid}">
       <input class="form-control" name="username" type="text" 
         placeholder="Username" required ng-model="session.username" />
       <span class="help-block" 
         ng-show="loginForm.username.$error.required">Required</span>
     </div>
     <div class="form-group"
       ng-class="{'has-error': loginForm.password.$invalid}">
       <input class="form-control" name="password" type="password" 
         placeholder="Password" required ng-model="session.password" />
       <span class="help-block" 
         ng-show="loginForm.password.$error.required">Required</span>
     </div>
     <div class="form-group">
         <button type="submit" class="btn btn-primary pull-right" 
           value="Login" title="Login">
           <span>Login</span>
         </button>
     </div>
   </form>

Okay, so not much changed there really. The disabled call to action though always bugs me. And once we solve our second problem, it will show off its usefulness a bit more. The next problem is we are highlighting invalid fields before the user has done anything which could be a bit confusing. Ideally we should only highlight the fields if the user has interacted with the form. AngularJS has some built-in support for this as well. On the ngFormController there is a $dirty property and $pristine (basically the opposite of $dirty) property. So we can add that as an additional condition on our ‘error‘ css class. Something like:

ng-class="{'has-error': loginForm.username.$invalid && loginForm.username.$dirty}"

But this still isn’t exactly what we want. The field will get highlighted after it’s dirty (i.e. the user has interacted with it), but what if we attempt submission? Technically the form isn’t dirty so the field won’t be highlighted. We need to track another state of the form which isn’t currently being tracked: attempted submission. In the ideal scenario, erroneous fields should only be highlighted if the user has tried to interact with the field or if the user has attempted to submit the form. First, lets track attempted. We could try to expand on the current custom rcSubmit directive, but I can foresee using this directive independent of it at some point so I’m going to make a separate directive, ‘rcAttempt‘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    var rcAttemptDirective = {
        'rcAttempt': function () {
            return {
                restrict: 'A',
                controller: ['$scope', function ($scope) {
                    this.attempted = false;
 
                    this.setAttempted = function() {
                        this.attempted = true;
                    };
                }],
                link: function(scope, formElement, attributes, attemptController) {
                    formElement.bind('submit', function (event) {
                        attemptController.setAttempted();
                        if (!scope.$$phase) scope.$apply();
                    });
                }
            };
        }
    };

In the code above you can see we create a custom controller with a property to track if form submission has been attempted. Then, like our rcSubmit, we bind to the form’s submit event, and set the ‘attempted‘ flag there. We can add this directive to our form element, but it won’t really do anything at this point. We are only tracking the state, but are not doing anything with it. We need a way access this information in the markup just like other form properties. The way to do that is by putting our new controller on the scope:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    var rcAttemptDirective = {
        'rcAttempt': function () {
            return {
                restrict: 'A',
                controller: ['$scope', function ($scope) {
                    this.attempted = false;
 
                    this.setAttempted = function() {
                        this.attempted = true;
                    };
                }],
                compile: function(cElement, cAttributes, transclude) {                    return {                        pre: function(scope, formElement, attributes, attemptController) {                            scope.rc = scope.rc || {};                            scope.rc[attributes.name] = attemptController;                        },                        post: function(scope, formElement, attributes, attemptController) {                            formElement.bind('submit', function (event) {                                attemptController.setAttempted();                                if (!scope.$$phase) scope.$apply();                            });                        }                    };                }            };
        }
    };

We use the ‘compile‘ function to manipulate the scope just as we would if we manipulate the DOM on ‘compile‘. We add an ‘rc‘ object, again to avoid conflicts with other scope values then, mimicking the ngFormController, use the form name. So when we access it in the markup it will look like: ‘rc.loginForm‘. We also move the binding to the post-linking function of ‘compile‘ because if you have a ‘compile‘ defined, the ‘link‘ function with be ignored. So now that the hard work is done we just need to add the directive and change our ‘ng-class‘ to use the new attempted state we are tracking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
   <form name="loginForm" novalidate 
     ng-app="LoginApp" ng-controller="LoginController" 
     rc-attempt rc-submit="login()">
     <div class="form-group" 
       ng-class="{'has-error': loginForm.username.$invalid && 
         (loginForm.username.$dirty || rc.loginForm.attempted)}">
       <input class="form-control" name="username" type="text" 
         placeholder="Username" required ng-model="session.username" />
       <span class="help-block" 
         ng-show="loginForm.username.$error.required">Required</span>
     </div>
     <div class="form-group" 
       ng-class="{'has-error': loginForm.password.$invalid &&
         (loginForm.password.$dirty || rc.loginForm.attempted)}">
       <input class="form-control" name="password" type="password" 
         placeholder="Password" required ng-model="session.password" />
       <span class="help-block" 
         ng-show="loginForm.password.$error.required">Required</span>
     </div>
     <div class="form-group">
         <button type="submit" class="btn btn-primary pull-right" 
           value="Login" title="Login">
           <span>Login</span>
         </button>
     </div>
   </form>

Success! So now if we open the page fresh, the form is clean. If the user manipulates a particular field and leaves it invalid, only that particular field will be highlighted. If the user attempts to submit the form, all invalid fields will be highlighted. Here is a link to the Plunker to see it in action: View Plunker.

Custom directives in AngularJS can be very powerful, and the design of AngularJS encourages building modular components. You can see this in production on all of our forms, such as the Register form.

Note: The source for all the directives and examples can be found on our public GitHub repo: https://github.com/realcrowd/angularjs-utilities

Quick Update: Refinement and Bonus Method. As with most code, we have refined and enhanced as we’ve moved forward. Here are a couple small changes we’ve made since writing this blog. First, the separation of rcAttempt and rcSubmit ended up being unduly complex for our needs. We therefore moved the functionality in rcAttempt into rcSubmit so we now just need one directive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    var rcSubmitDirective = {        'rcSubmit': function () {            return {                restrict: 'A',                require: ['rcSubmit', '?form'],                controller: ['$scope', function ($scope) {                    this.attempted = false;                     this.setAttempted = function() {                        this.attempted = true;                    };                }],                compile: function(cElement, cAttributes, transclude) {                    return {                        pre: function(scope, formElement, attributes, controllers) {                             var submitController = controllers[0];                             scope.rc = scope.rc || {};                            scope.rc[attributes.name] = submitController;                        },                        post: function(scope, formElement, attributes, controllers) {                             var submitController = controllers[0];                            var formController = (controllers.length > 1) ?                                                  controllers[1] : null;                             var fn = $parse(attributes.rcSubmit);                             formElement.bind('submit', function (event) {                                submitController.setAttempted();                                if (!scope.$$phase) scope.$apply();                                 if (!formController.$valid) return false;                                 scope.$apply(function() {                                    fn(scope, {$event:event});                                });                            });                        }                    };                }            };        }    };

We now no longer need the rc-attempt attribute in our mark-up. Second, we noticed repetitive pattern in our class logic and decided to make a small utility method on our rcSubmitController (formly rcAttemptController) to make the markup a little cleaner. That method needs some additional information to, so we’ll need the following changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    var rcSubmitDirective = {
        'rcSubmit': function () {
            return {
                restrict: 'A',
                require: ['rcSubmit', '?form'],
                controller: ['$scope', function ($scope) {
                    var formController = null;                     this.setFormController = function(controller) {                        formController = controller;                    };                     this.needsAttention = function (fieldModelController) {                        if (!formController) return false;                         if (fieldModelController) {                            return fieldModelController.$invalid &&                                    (fieldModelController.$dirty || this.attempted);                        } else {                            return formController && formController.$invalid &&                                   (formController.$dirty || this.attempted);                        }                    };

30
31
32
33
34
35
36
37
38
39
40
41
42
                compile: function(cElement, cAttributes, transclude) {
                    return {
                        pre: function(scope, formElement, attributes, controllers) {
 
                            var submitController = controllers[0];
 
                            var formController = (controllers.length > 1) ?                                                  controllers[1] : null;                            submitController.setFormController(formController); 
                            scope.rc = scope.rc || {};
                            scope.rc[attributes.name] = submitController;
                        },

In order for our utility method to work, we needed a reference to the ngFormController. Then our method accepts a ngModelController for a field and we do our logic. So now our class logic can be written as:

ng-class="{'has-error': rc.loginForm.needsAttention(loginForm.username)}

Just a couple small little bonuses to keep your code looking clean. The examples and code have been updated on Plunker and GitHub