Using AngularJS with the Bootstrap CSS framework makes it trivially simple to implement form validation on your site. You just need to use the ngClass directive to apply the ‘has-error’ class when the input value is invalid. Here’s an example:

  <form name="userForm">
    <div class="form-group" ng-class="{ 'has-error': userForm.email.$invalid }" >
      <input type="email" class="form-control" name="email" ng-model="user.email" required />
    </div>
  </form>

What is not trivially simple is to have your form validation actually provide a good user experience (e.g. the input box above is highlighted invalid before the user even has a chance to enter anything).

So I want to share the process I went through to create a custom Angular directive that provides a great user experience. Others have also written about creating better experiences for form validations, but I have still found them lacking.

The directive itself is called showErrors, and it can be downloaded through Bower with angular-bootstrap-show-errors. The code is located on Github at https://github.com/paulyoder/angular-bootstrap-show-errors

First Attempt

As I already mentioned, most people’s first attempt at form validation with AngularJS involves the ngClass directive coupled with the $invalid property on the form control. It looks something like this:

<div class="form-group" ng-class="{ 'has-error': userForm.email.$invalid }">

And here is a complete example along with a preview of how it behaves.

<form name="userForm">
  <div class="form-group" ng-class="{ 'has-error': userForm.name.$invalid }">
    <input type="text" class="form-control" name="name" ng-model="user.name" required>
  </div>
  <div class="form-group" ng-class="{ 'has-error': userForm.email.$invalid }">
    <input type="email" class="form-control" name="email" ng-model="user.email" required>
  </div>
</form>

This does the basic job of displaying which fields are invalid, but as a user it’s really annoying being shown which fields are invalid before you even have the chance to fill out the form.

Dirty Checking

Fortunately we can use the $dirty flag that AngularJS provides to wait until the user enters information before showing the invalid entries. So we’ll update the ngClass directive with the following logic.

<div ng-class="{ 'has-error': userForm.email.$invalid && userForm.email.$dirty }" class="form-group">

This works great for the Name text box, but now when the user starts typing in the email, it shows an error after the user enters the first letter. This is because a single letter is not a valid email address, but the $dirty flag is true because the user has entered text. What we want is to wait until the user has finished entering the full email address before we perform the validation.

Waiting to validate the input box until after the user has finished entering the full email address isn’t possible with just the ngClass directive, so we’ll need to create a custom directive to help us.

After Finished Editing

It’s pretty easy to create a directive that will add the ‘has-error’ class after the user leaves the text box.

angular.module('app', []).
  directive('showErrors', function() {
    return {
      restrict: 'A',
      link: function(scope, el) {
        el.bind('blur', function() {
          var valid = // is valid logic
          el.toggleClass('has-error', valid);
        });
      }
    }
  });

We still have a couple issues we need to overcome in this directive though. First, the ‘has-error’ class for Bootstrap is not added to the text box itself, but rather to the surrounding div with the ‘form-group’ class. And second, the directive needs to know if the text box has invalid input.

This means a couple things for the directive. It needs to be placed on the div with the ‘form-group’ class, and it needs access to the form controller so it knows if the text box is invalid or not.

So our html now looks like this:

  <form name="userForm">
    <div class="form-group" show-errors>
      <input type="text" class="form-control" name="name" ng-model="user.name" required />
    </div>
    <div class="form-group" show-errors>
      <input type="email" class="form-control" name="email" ng-model="user.email" required />
    </div>
  </form>

And our directive looks like this:

angular.module('app', []).
  directive('showErrors', function() {
    return {
      restrict: 'A',
      require:  '^form',
      link: function (scope, el, attrs, formCtrl) {
        // find the text box element, which has the 'name' attribute
        var inputEl   = el[0].querySelector("[name]");
        // convert the native text box element to an angular element
        var inputNgEl = angular.element(inputEl);
        // get the name on the text box so we know the property to check
        // on the form controller
        var inputName = inputNgEl.attr('name');

        // only apply the has-error class after the user leaves the text box
        inputNgEl.bind('blur', function() {
          el.toggleClass('has-error', formCtrl[inputName].$invalid);
        })
      }
    }
  });

Now this provides a much better user experience since the validation errors aren’t shown until after the user has finished entering data into the form.

Manually Checking for Errors

An additional scenario we need to account for is when the user tries to save the form before entering information into all the form fields. The user doesn’t know the form is invalid yet because they haven’t entered any information. Which means our directive won’t show the invalid fields.

A common practice for this situation is to disable the Submit/Save button on the form until the form is valid.

<button class="btn btn-primary" ng-disabled="userForm.$invalid">Save</button>

I highly discourage this practice though, because it provides a horrible experience for the user. They see that the save button is disabled, but they have no idea why. So you’re forcing the user to think and try to figure out why they can’t save the form. Apps should never force the user to try to figure out why they can’t do some action. The app should always tell the user why they can’t perform an action.

So a better experience is to always have the save button enabled. Then if the user tries to save the form before it’s valid, we can show the invalid fields. Let’s update the directive to allow for this.

Basically we’ll need the ability to force the directive to check the validity even if the user hasn’t entered information yet. To do this, the controller broadcasts the ‘show-errors-check-validity’ event. The showErrors directive subscribes to this event and will check the validity when it is fired.

Here’s an example of how that looks:

// inside the directive's link function from the previous example
scope.$on('show-errors-check-validity', function() {
  el.toggleClass('has-error', formCtrl[inputName].$invalid);
});

And we’ll have the Add User button call the save() function on the controller.

<button class="btn btn-primary" ng-click="save()">Add User</button>

And the save function will broadcast the show-errors-check-validity event which will cause the directive to show the fields with validation errors.

// in the controller
$scope.save = function() {
  $scope.$broadcast('show-errors-check-validity');

  if ($scope.userForm.$invalid) { return; }
  // code to add the user
}

This is looking pretty good now. The only thing left to implement is the reset functionality.

Reset

The reset link currently clears out any input in the textboxes, but it doesn’t hide any form validation errors that were previously being shown. You can see what I mean by entering an invalid email and then click the reset link. The email texbox is cleared, but it still shows there is an error.

To account for this, we’ll do something similar to what we did for manually checking the validity. The controller will broadcast the show-errors-reset event and the directive will remove the has-error class from the DOM.

// inside the directive's link function from the previous example
scope.$on('show-errors-reset', function() {
  $timeout(function() {
    el.removeClass('has-error');
  }, 0, false);
});

You probably noticed the $timeout function was used to remove the ‘has-error’ class. This is because we need to wait until the current digest cycle has finished before removing the class. Else there’s a race condition that will keep the class from being removed.

Showing Validation Messages

And to put the finishing touch on, let’s show the user an error message that explains why the field is invalid. Fortunately we don’t have to change the directive to do this. We just need to add the error message and a few lines of CSS.

In the html we’ll add a paragraph with a ‘help-block’ class and only show it if the property has an error.

  <form name="userForm">
    <div class="form-group" show-errors>
      <input type="text" class="form-control" name="name" ng-model="user.name" required />
      <p class="help-block" ng-if="userForm.name.$error.required">The user's name is required</p>
    </div>
    <div class="form-group" show-errors>
      <input type="email" class="form-control" name="email" ng-model="user.email" required />
      <p class="help-block" ng-if="userForm.email.$error.required">The user's email is required</p>
      <p class="help-block" ng-if="userForm.email.$error.email">The email address is invalid</p>
    </div>
  </form>

And the CSS we add will hide the error message unless the has-error class is present.

.form-group .help-block {
  display: none;
}

.form-group.has-error .help-block {
  display: block;
}

Showing Valid Entries

I also recently updated the directive to show the user valid entries by applying the show-success class on valid input values. The demo below shows how the Name field will turn green after you enter a name and tab out of it.

There’s two ways to configure the directive to show valid entries. You can configure it on every input box by passing in the `{showSuccess: true}’ option.

<div class='form-group' show-errors='{ showSuccess: true }'>
  <input type='text' name='firstName' ng-model='firstName' class='form-control' required />
</div>

Or you can configure it globally by using the showErrorsConfigProvider

app = angular.module('yourApp', ['ui.bootstrap.showErrors']);
app.config(['showErrorsConfigProvider', function(showErrorsConfigProvider) {
  showErrorsConfigProvider.showSuccess(true);
});

Conclusion

And now we have a custom AngularJS directive that provides a great user experience for form validation errors. And the great part is how easy it is to add this to any form.

Feel free to use this directive in your project, and let me know of any further optimizations you make to the directive as well.


Navigation