Taming Forms in AngularJS 1.3

Learn how to use the new form features in AngularJS 1.3

Sep 2, 2014

Forms in AngularJS 1.3 have been improved quite a bit from what they were back in 1.2 and 1.1. Lots of bugs for native HTML5 validators have been resolved and new features such as the validators pipeline and asynchronous validations have also been introduced. Packaged together with nested forms and the new ngMessages module, AngularJS 1.3 is more powerful than ever when it comes to developing forms.

Let's have a look at these amazing new features and craft together some amazing user input forms!

1.0 Form Validation in Angular

Form validation in every framework is a different beast. With vanilla JavaScript it's even more unpredictable. There has been some progress with the HTML5 form validation API, but overall, the API is inconsistent and not all devices support it well enough and some platforms don't even support it at all.

Thankfully AngularJS has made a versatile wrapper for this via ngModel. The internals of ngModel only use the HTML5 validation API if needed, but overall the consistency between browsers is very good. And with AngularJS 1.3 the level of control is much more advanced than with 1.2.

There's a lot to digest here so let's get started...

to top

2.0 Presentation Video, Github Repo and Demo Application

Not too long ago, back in June of 2014, at the Toronto AngularJS Meetup, I presented on form validation with AngularJS and covered just about all of the topics here in this article. The video is visible below via Youtube embed. Please keep in mind that, at the time of the presentation, the asynchronous validation API was not stable yet. So when the video itself is discussing how the API works for promised-based validations, have a look at how they're done here in the article afterwards. Otherwise, enjoy the video! I hope you laugh at my cheesy humour.

And there is also a companion demo repo that showcases all the features outlined in this article. The links to the live demo and code repo can be found below...

http://yom.nu/ng-forms-demo http://yom.nu/ng-forms-code

to top

3.0 The Basics & Fun

It's unfair to assume that everyone knows how forms work in Angular. That being said, let's partake in a brief intro to how they work.

3.1 Everything that collects data uses ngModel

It's is likely that everyone who has played with an Angular application has seen the ng-model attribute that sits on HTML input elements. This magical directive bridges the gaps between the input elements in the DOM world over to the scope and controller world. The bridge itself is called a two-way data-binding and it effectively means that if the input field is updated by the user then the scope data (the model value) will update. Then if the model value is updated then the DOM input value will also be updated.

http://plnkr.co/edit/hyJXJFVHJPWvQYo0A7bM?p=preview

<div class="field">
  <input type="text" ng-model="myName" />

  My name is ****

  <button ng-click="myName='default value'">
    Reset to 'default value'
  </button>
</div>

Now when the user enters a value into the input element the scope value will reflect what's in the input field. When the button is clicked then the scope value changes first and the input field then reflects what the scope value is.

3.2 There's the model value and the model controller

The model value reflects the scope data which is tied to the input field. But the logic of passing values back and forth between the DOM and the scope is all managed by a well constructed controller called ngModelController. Each time an input element is tied together with the ng-model attribute then the ngModel directive will create an instance of ngModelController and then all of the parsing, formatting, validating and transporting of data will be handled by the controller.

The beauty of this setup is that we barely need any JavaScript code to handle standard things such as passing data to the scope, performing validations and firing model-specific events. Have a look at the code below:

<div class="field">
  <input type="email"
         minlength="5"
         maxlength="100"
         ng-model="myEmail"
         required />
</div>

The input element above is tied to a model value represented by the scope property myEmail. When data is written by the user then the scope value will eventually be populated with the value. The input element HTML code is also declaring a series of validation constrictions that must be met _before_the value is written to scope by Angular. The ngModelController in this case will check to see that the value is entered by the user (required), the value has a minimum length of 5 (minlength) but also a maximum length of 100 (maxlength) and that it also is an email field (the email type attribute specifies this). These four validation properties save us tons of JS code and allow us to use the same validators on other input elements within our form code.

When a validator fails then it registers it's error on the instance of the ngModelController on the $error object. However before we can display errors we need to first get ahold of the model within the template. This can be achieved by first wrapping the model inside of a form and providing a name value to both the form and the input element.

<form name="myForm">
  <div class="field">
    <input type="email"
           name="myEmail"
           minlength="5"
           maxlength="100"
           ng-model="myEmail"
           required />
  </div>
</form>

We can now examine the validation status of the model via myForm.myEmail.$error. We can also examine other properties on the model such as $pristine, $dirty, $valid, and $invalid. Having access to the model within the template allows for us to display error messages whenever any of these values change.

<form name="myForm">
  <div class="field">
    <input type="email"
           name="myEmail"
           minlength="5"
           maxlength="100"
           ng-model="myEmail"
           required />

    <div ng-if="myForm.myEmail.$invalid">There is an error with the field...</div>
  </div>
</form>

We can also gain access to the instance of the ngModelController within directive code where we can place custom validations and create custom ngModel components. The trick to doing so is to include the ngModel controller into our directive using require within our directive definition block. The newly created directive must be placed on an element that also has ng-model on it.

//
// Use this in your templates like so
// <input type="text" custom-validator ng-model="myModel" />
//
ngModule.directive('customValidator', function() {
  require : 'ngModel',
  link : function(scope, element, attrs, ngModel) {
    ngModel.$validators.myValidator = function() { ... }
  } 
});

Just like individual models, the containing form also has its own controller. The form controller is designed to manage the validity of the entire form and it also has the $valid and $invalid properties. The example below shows a controller that decides whether or not to submit the form data to the backend depending on if the form status is valid.

ngModule.controller('FormCtrl', function($http) {
  this.submit = function(isValid, data) {
    if(!isValid) return;

    //submit the data to the server
    $http.post('/api/submit', data);
  }
});

The form template then calls the controller's submit method via the ng-submit attribute:

<form ng-controller="FormCtrl as form"
      name="myForm" 
      ng-submit="form.submit(myForm.$valid, data)">
  <div class="field">
    <input type="email"
           name="myEmail"
           minlength="5"
           maxlength="100"
           ng-model="data.myEmail"
           required />

    <div ng-if="myForm.myEmail.$invalid">There is an error with the field</div>
  </div>
</form>

Notice how we used data as a collection to store the contents of the model? If we store all of our model data within a single collection then the controller can trust that collection data and send it off to the server. This pattern allows us to save a lot of JavaScript code.

Hopefully this intro shed some light on to the basics of ngModel. Let's continue to the next step of the article and start to learn some of the cutting edge features of AngularJS 1.3!

to top

4.0 HTML5 Validators & Parse Errors

Prior to AngularJS 1.3, only certain input attributes would trigger a validator to be registered on the model. Now all of the HTML5 validation attributes are properly bound to ngModel and the errors are registered on ngModel.$error whenever the validation fails. The table below shows each of the attributes and what error will be triggered upon failure:

HTML5 Attribute ng Attribute Registered Error
required="bool" ng-required="..." ngModel.$error.required
minlength="number" ng-minlength="number" ngModel.$error.minlength
maxlength="number" ng-maxlength="number" ngModel.$error.maxlength
min="number" ng-min="number" ngModel.$error.min
max="number" ng-max="number" ngModel.$error.max
pattern="patternValue" ng-pattern="patternValue" ngModel.$error.pattern

The following input types are also automatically registered for validation:

<input type="..."> Registered Error
type="email" ngModel.$error.email
type="url" ngModel.$error.url
type="number" ngModel.$error.number
type="date" ngModel.$error.date
type="time" ngModel.$error.time
type="datetime-local" ngModel.$error.datetimelocal
type="week" ngModel.$error.week
type="month" ngModel.$error.month

4.1 How ngModel handles these errors

Due to the nature of HTML5 form validation (the native DOM API), some input types may not expose the input value (yes element.value) until the valid input value is entered into the field by the user. This is the case for the number and each of the date input field types.

With 1.3, ngModel now does a consistent job of handling these situations by handling parse-related validations first and then the rest of the validations. This means that if an invalid number, date, url or email is entered then ngModel will display that error first before handling validators such as required, minlength, max and so on.

http://plnkr.co/edit/UuF2H1poVvPfiePEhDif?p=preview)

to top

5.0 The $validators pipeline

AngularJS 1.3 uses $validators instead of $parsers + $formatters for performing validations. This means that when it comes to creating custom validators, we must first create a directive and then include ngModel in order to register the validator onto ngModel.$validators. The code below shows an example of a password validator that checks to see if the input value matches required range of characters for a password field.

https://github.com/yearofmoo-articles/AngularJS-Forms-Article/blob/master/frontend/app.js#L27

ngModule.directive('validatePasswordCharacters', function() {

  var REQUIRED_PATTERNS = [
    /\d+/,    //numeric values
    /[a-z]+/, //lowercase values
    /[A-Z]+/, //uppercase values
    /\W+/,    //special characters
    /^\S+$/   //no whitespace allowed
  ];

  return {
    require : 'ngModel',
    link : function($scope, element, attrs, ngModel) {
      ngModel.$validators.passwordCharacters = function(value) {
        var status = true;
        angular.forEach(REQUIRED_PATTERNS, function(pattern) {
          status = status && pattern.test(value);
        });
        return status;
      }; 
    }
  }
});

Notice how the validation function is expected to return a boolean value? That's all that is required when it comes to the validators pipeline.

And the HTML code for the input elements looks like so:

https://github.com/yearofmoo-articles/AngularJS-Forms-Article/blob/master/frontend/form.html#L55

<form name="myForm">
  <div class="label">
    <input name="myPassword" type="password" ng-model="data.password" validate-password-characters required />
    <div ng-if="myForm.myPassword.$error.required">
      You did not enter a password
    </div>
    <div ng-if="myForm.myPassword.$error.passwordCharacters">
      Your password must contain a numeric, uppercase and lowercase as well as special characters
    </div>
  </div>
</form>

Now what about if we wanted to render an asynchronous validation? You something to like check to see if the provided value is valid against the backend database? Can we just return a promise inside of the validation function? Well if we use $asyncValidators then we can do just that. Let's see how that's possible.

to top

6.0 Asynchronous Validation via $asyncValidators

Let's follow the same approach as the example above and create a validator that will check to see if the user entered username is available by performing an AJAX request against the backend.

https://github.com/yearofmoo-articles/AngularJS-Forms-Article/blob/master/frontend/app.js#L27

ngModule.directive('usernameAvailableValidator', ['$http', function($http) {
  return {
    require : 'ngModel',
    link : function($scope, element, attrs, ngModel) {
      ngModel.$asyncValidators.usernameAvailable = function(username) {
        return $http.get('/api/username-exists?u='+ username);
      };
    }
  }
}])

The $asyncValidators pipeline expects each of the validators to return a promise when a validation is kicked off. When the promise fulfills itself then the validation is successful and when it rejects then a validation error is registered. Only once all of the normal validators and the async validators have successfully passed then the model value will be written to the scope.

One thing to keep in mind is that asynchronous validations will NOT run unless all of the prior normal validators (the validators present inside of ngModel.$validators) have passed. This constriction allows for the developer (yes you) to prevent the validator from making excessive backend calls when the username input data is invalid. The example HTML code below explains this better:

https://github.com/yearofmoo-articles/AngularJS-Forms-Article/blob/master/frontend/form.html#L33

<form name="myForm">
<!-- 
  first the required, pattern and minlength validators are executed
  and then the asynchronous username validator is triggered...
-->
<input type="text"
       class="input"
       name="username"
       minlength="4"
       maxlength="15"
       ng-model="form.data.username"
       pattern="^[-\w]+$"
       username-available-validator
       placeholder="Choose a username for yourself"
       required />
<!-- ... -->
</form>

6.1 Is the model and/or form still valid during validation? Or invalid?

Both the model and the form set the $valid and $invalid flags to undefined once one or more asynchronous validators have been triggered and then, once everything has been resolved, the $valid and $invalid flags will be restored based on the combined validity status of each of the asynchronous validators. During this time a special object called $pending will be set on both the model and the form to identify which asynchronous validations are ongoing. Once everything is complete then the pending object will be removed.

6.2 Showing a loading animation

Since the $pending object is set on the model this means that a loading animation can be placed beside the model to indicate when the asynchronous operation (like the username HTTP request) is underway.

<form name="myForm">
  <!-- first the required, pattern and minlength validators are executed
       and then the asynchronous username validator is triggered -->
  <input type="text"
         name="myUsername"
         ng-model="data.username"
         minlength="10"
         pattern="^[-\w]+$"
         validate-username-availability
         required />
  <div ng-if="myForm.myUsername.$pending">
    Checking Username...
  </div>
</form>
to top

7.0 Rendering error messages

By default, Angular will render all errors on the template as soon the form is ready (when it's compiled). There are multiple ways to change this behaviour.

7.1 Using ngIf or ngShow/ngHide

Whenever an input field is focussed on and then blurred the $touched property will be set to true. Therefore if we place an ngIf or ngShow statement on a container surrounding the error messages then we can control when they're displayed.

<form name="myForm">
  <input type="text" name="colorCode" ng-model="data.colorCode" minlength="6" required />
  <div ng-if="myForm.colorCode.$touched">
    <div ng-if="myForm.colorCode.$error.required">...</div>
    <div ng-if="myForm.colorCode.$error.minlength">...</div>
    <div ng-if="myForm.colorCode.$error.pattern">...</div>
  </div>
</form>

But what about if the form is submitted first? Shouldn't all the errors show up then? We can fix this by using the myForm.$submitted flag that is set when the form is submitted.

<form name="myForm">
  <input type="text" name="colorCode" ng-model="data.colorCode" minlength="6" required />
  <div ng-if="myForm.$submitted || myForm.colorCode.$touched">
    <div ng-if="myForm.colorCode.$error.required">...</div>
    <div ng-if="myForm.colorCode.$error.minlength">...</div>
    <div ng-if="myForm.colorCode.$error.pattern">...</div>
  </div>
</form>

Now the form is a bit more user friendly. But what happens when we have too many errors? How can we control the behaviour of what error messages show up and when? How can we setup a priority of which errors show up first? AngularJS 1.3 also comes with support for this via a new module called ngMessages.

7.2 Using ngMessages

Let's change around the HTML code from before and use ngMessages to display the errors.

<form name="myForm">
  <input type="text" name="colorCode" ng-model="data.colorCode" minlength="6" required />
  <div ng-messages="myForm.colorCode.$error" ng-if="myForm.$submitted || myForm.colorCode.$touched">
    <div ng-message="required">...</div>
    <div ng-message="minlength">...</div>
    <div ng-message="pattern">...</div>
  </div>
</form>

When the messages are displayed, only one message will show up at a time. The ng-messages directive keeps track of the order of the error messages and it only renders the next message once the error is gone from the model (the $error object). (Now keep in mind we're still using the same ng-if code from before on the container element to control when the messages are displayed.)

Also, before this code actually works, we need to download the angular-messages.js JavaScript file and include ngMessages as a dependency for our application.

<script type="text/javascript" src="angular-messages.js"></script>
<script type="text/javascript">
var ngModule = angular.module('myApp', ['ngMessages']);
</script>

click here for more

to top

8.0 Controlling when the model value updates

AngularJS 1.3 has a new feature called ngModelOptions which is basically an extra attribute that allows for extra configurations to be applied to how ngModel works on the input element. One particularly useful feature is something called value debouncing. This feature allows for the input field to be configured to only update the model based on certain input events. The default behaviour is that the input field will update ngModel each time the user types in a single character.

Let's say for example that we wanted to only perform asynchronous validations once all the validators are passed in and the user has not typed in anymore characters for 500 milliseconds.

<input type="text"
       name="myUsername"
       ng-model="data.username"
       minlength="10"
       pattern="^[-\w]+$"
       validate-username-availability
       ng-model-options="{ debounce : { 'default' : 500 } }"
       required />

Now the validator won't be kicked off until things go quiet in terms of user input by half a second. But can we extend this further? Can we make it so that the model value and validations are applied immediately after when the user blurs out of the field? This can be handled by setting a wait time of 0ms for the blur event.

ng-model-options="{ debounce : { 'default' : 500, blur : 0 } }"

Excellent stuff. This means that if there are any errors visible then they _will not evaluated_until the debounce operation is over (this is after the timeout or when the user jumps out of the field).

Another super useful feature of deboucing with ngModelOptions is to control when the URL of page changes due to a search querysting being applied when the user searches for something. Watch the video provided above to see what I am talking about.

to top

9.0 Dynamic & Repeatable Fields + Nested Forms

When ng-if was introduced it brought along the ability to add and remove subsections of forms very easily. And by doing so new, optional, form input components can be added and removed and the overall validity of the form would update according to the change of which fields are present and which fields are not. Imagine if we built something like a calendar that collected an email address only if the user wanted to provide an email address to by notified about the event. How would the HTML code look like?

<form>
  <div class="field">
    <label>
      <input type="checkbox" ng-model="data.allowNotifications" />
      Notify me via email 30 minutes before my event happens
    </label>
  </div>
  <div class="field" ng-if="data.allowNotifications">
    <label>Notification Email:</label>
    <input type="email" ng-model="data.notificationEmail" name="notificationEmail" required />
  </div>
  <input type="submit" />
</form>

This may all seem trivial--and it is--but the key thing to pay attention to in the HTML code is that the email input field will only show up when the checkbox is true. Yes this is still super basic, but since the input field is being inserted and removed from the DOM whenever the checkbox model changes, the validity of the input field will also be applied against the form. So in other words, when the field is visible then it will render the form as invalid since the field is required, but when it is removed then the form is valid again.

9.1 Nested Forms Repeatable Forms

Let's now imagine that instead of having one email we wanted to have multiple. How can we change around the HTML code allow for multiple emails to be collected and still make the validation of the form work as normal? We can use ng-repeat inside and apply the data to the repeated item.

<div class="field" ng-if="data.allowNotifications">
  <div ng-repeat="email in notifcationEmails"> 
    <label>Email NaN:</label>
    <input type="email" ng-model="email" name="notificationEmail" />
  </div>
</div>

But what happens if the user enters an invalid email address? How can we show individual errors for each repeated item? The trick here is to use nested forms via the ng-form attribute on the element that is being repeated.

<div class="field" ng-if="data.allowNotifications">
  <div ng-form="emailForm" ng-repeat="email in notifcationEmails"> 
    <label>Email NaN:</label>
    <input type="email" ng-model="email" name="notificationEmail" />
    <div ng-if="emailForm.notificationEmail.$error.email">
      You did not enter a valid email address
    </div>
  </div>
  <button ng-click="addAnotherEmail()">Add another email</button>
</div>

Awesome! But wait! Now there are wayyyy to many errors showing up. We can use ngMessages to clean things up but is it possible to only show one error for all the repeated items together? If we place another ng-form attribute, but on the container element, then we can certainly make it work.

<div class="field" ng-if="data.allowNotifications" ng-form="notificationEmails">
  <div ng-form="emailForm" ng-repeat="email in notifcationEmails"> 
    <label>Email NaN:</label>
    <input type="email" ng-model="email" name="notificationEmail" />
    <div ng-if="emailForm.notificationEmail.$error.email">
      You did not enter a valid email address
    </div>
  </div>

  <div ng-if="notificationEmails.$error.email" class="error">
    One or more emails have been incorrectly entered.
  </div>

  <button ng-click="addAnotherEmail()">Add another email</button>
</div>

Perfect!

to top

10.0 What about Parsers / Formatters?

While parsers and formatters are not any different in AngularJS 1.3, the are not meant to handle any validation logic. Instead, parsers are designed to convert the view value into a different model value and formatters are for converting the model value into the appropriate view value. What's the difference between the model value and the view value? The model value is what gets placed on the scope and the view value is what exists within the DOM input element.

A good example of the use parsers in Angular is with how Angular internally handles date-based input elements (like date, time, datetime, week, month, etc...). What goes on here is the view value (entered by the user) is a string based value, but when it reaches the scope the value itself is an instance of Date. The below is a simplified snippet of date parsing in action:

var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;

//grab ngModel inside of a directive
ngModel.$parsers.push(function(value) {
  if (value == '' || value == null || value == undefined) {
    // null means that there is no value which is fine
    return null;
  }

  if (DATE_REGEXP.test(value)) {
    return parseDateFromString(value);
  }

  // undefined means that the date syntax is invalid and
  // this will cause a parse error during validation
  return undefined;
});

Formatters work in the opposite way. So if we wanted to convert a date to a properly formatted string then we would just do this:

//grab ngModel inside of a directive
ngModel.$formatters.push(function(value) {
  if(angular.isDate(value)) {
    value = $filter('date')(value, ['yyyy', 'MM', 'dd']);
  }
  return value;
});

Now the input field will properly update itself with the string-based value while the scope value is an actual date object.

to top

11.0 A Work in Progress

ngModel has been around since the start of AngularJS and the new features in 1.3 have definetly improved upon the experience for both developers and users. Some of the new features may have a tiny quirk or two. These kinks will be fixed in 1.3 stable, but now with the release of RC0 we can be comfortable to use these new features since the API has been decided upon. So go ahead... Don't be shy :)

If you happen to find any bugs or have any ideas in mind, please open a new issue on the AngularJS repo on Github and, within your issue description, write the message to include @matsko in the content. That way I will be able to see what you've typed :)

Thanks so much for reading all of this. If you don't mind, please share the article! And please follow @yearofmoo on Twitter!

to top

Up Next