ngMessages is a new feature in AngularJS 1.3 for rending error messages in forms
Forms in AngularJS are a delight to work with since they naturally work off of the basics of how forms work in HTML. The ngModel directive peacefully works with form controls and the state of a form can easily be examined using the name of the form and the name of each input control. But what about displaying error messages? Since there is no set way of displaying error messages in forms, there are many inconsistent ways to do this in AngularJS. While showing and hiding a message is easy using ngIf or ngSwitch, making use of multiple messages across and entire application is where things get messy. Do we really have to use ngIf over 20 times for a form just to show a handful of messages? How can we reuse a template of messages in other parts of the application? Are forms really this complicated?
The new ngMessages module introduced in AngularJS 1.3-beta.8 is designed to render error messages in a reusable and maintainable way. Instead of having to butcher up your template code by placing ngIf statements everywhere, ngMessages listens on the model.$error
object and then decides which messages to display based on what is present in the template.
Interested in learning more? let's explore the ngMessages and ngMessage directives in detail.
Keep in mind that ngMessages is only available on AngularJS 1.3.0-beta.8 or higher. This feature is not available in 1.2 unfortunately.
to topIn addition to the tutorial, this article also provides a link to a demo form application that makes use of the ngMessages module. The demo isn't functional to the point that it submits data to the server, but it contains a collection of input controls that render error messages in a reusable manner.
Also, be sure to type in username into the username field to see how an asynchronous validation is applied.
Click here to see the demo application
to topThe real issue with error message management in AngularJS is the code required in the template to determine what is displayed. The template gets too heavy and too much if this and if that logic is put into the HTML code.
But enough of the complaining. Let's instead look at an example right away that displays an email address within a form managed by Angular.
<form name="userForm">
<div class="field">
<label for="emailAddress">Enter your email address:</label>
<input type="email" name="emailAddress" ng-model="data.email" required>
<div ng-if="userForm.emailAddress.$error.required" class="error">
You forgot to enter your email address...
</div>
<div ng-if="!userForm.emailAddress.$error.required &&
userForm.emailAddress.$error.email" class="error">You did not enter your email address correctly...
</div>
</div>
<input type="submit">
</form>
The input code is simple, but the error messages are complicated. In order to display the required error message first and then display the next error after, we need to setup a complex boolean structure to do this. Once we get past a couple of error messages then we may potentially need to setup more error message expressions. To get around this we may be able to get away with placing the logic inside of a controller...
<div ng-if="onlyHasError('required', myForm.emailAddress)"
class="error">
You forgot to enter your email address...
</div>
<div ng-if="onlyHasError('email', myForm.emailAddress)"
class="error">
You did not enter your email address correctly...
</div>
But now our template code is heavily dependent on the controller and scope. Not only that, but we can't reuse this stuff since the ng-if expression is specific to email input element. Let's stop here since our code is getting out of hand. Let's explore a different approach.
to topInstead of relying on complicated error message handling using ng-if expressions, let's make use of the ngMessages directive. First things first, let's include ngMessages into our application and attach the ngMessages module to our application module as a dependency.
<script type="text/javascript">
angular.module('myApp', ['ngMessages']);
</script>
Now we can rebuild our email form example. Let's strip out our ngIf messages and wrap everything inside a div element with the ngMessages directive and each individual message using the ngMessage directive.
<form name="userForm">
<div class="field">
<label for="emailAddress">
Enter your email address:
</label>
<input type="email" name="emailAddress" ng-model="data.email" required />
<div ng-messages="userForm.emailAddress.$error">
<div ng-message="required">
You forgot to enter your email address...
</div>
<div ng-message="email">
You did not enter your email address correctly...
</div>
</div>
</div>
<input type="submit">
</form>
And that's all the code we need!
The outer directive (ng-messages) listens on the userForm.emailAddress.$error object (which contains a key/value list of all the errors present on the email address model). As the state of that $error object changes then the ng-messages directive will examine its contents of inner directives (the ng-message directives) and select the first directive element (in this case required message). So, based on the example above, if the $error object is has the required property and that is true then the required message will be displayed. If not then the next ng-message directive will be examined (in this case the email message) and then that will be displayed if the error exists. Therefore, the order of the messages is prioritized within the template code and not in the controller.
Let's extend our validation scope and add minlength and maxlength validations to our email input element. How do we structure our message code now?
<form name="userForm">
<div class="field">
<label for="emailAddress">
Enter your email address:
</label>
<input type="email"
name="emailAddress"
ng-model="data.email"
ng-minlength="5"
ng-maxlength="30"
required />
<div ng-messages="userForm.emailAddress.$error">
<div ng-message="required">
You forgot to enter your email address...
</div>
<div ng-message="minlength">
Your email address is too short...
</div>
<div ng-message="maxlength">
Your email address is too long...
</div>
<div ng-message="email">
You did not enter your email address correctly...
</div>
</div>
</div>
<input type="submit">
</form>
This intuitive approach of displaying what errors show up and when is all handled by whatever shows up in the DOM first. Instead of having to rely on ngIf messages to setup complicated boolean expressions, ngMessages respects the order of the inner ng-message DOM elements and then acts accordingly. But what about if we wanted to display all the messages instead of just one at a time? This can be done by adding the ng-messages-multiple attribute to the ng-messages container.
<div ng-messages="userForm.emailAddress.$error"
ng-messages-multiple>
<div ng-message="required">...</div>
<div ng-message="minlength">...</div>
<div ng-message="maxlength">...</div>
<div ng-message="email">...</div>
</div>
Also, keep in mind that we can use element directives instead of attribtutes...
<ng-messages for="userForm.emailAddress.$error"
multiple="">
<ng-message when="required">
...
</ng-message>
<ng-message when="minlength">
...
</ng-message>
<ng-message when="maxlength">
...
</ng-message>
<ng-message when="email">
...
</ng-message>
</ng-messages>
This is much better than using a series of ngIf expressions. But we're still relying on an expression that is strictly tied to a particular model on the form. How can we possibly reuse these messages?
to topError messages can be reused within a ngMessages block by including a remote (or inline template) using the ng-message-include attribute. Let's create a generic template where the required, minlength and maxlength error messages are stored. First let's create a remote template file that contains our error messages.
<div ng-message="required">
You left the field blank...
</div>
<div ng-message="minlength">
Your field is too short
</div>
<div ng-message="maxlength">
Your field is too long
</div>
<div ng-message="email">
Your field has an invalid email address
</div>
And now let's create our form which will make use of the template..
<form name="userForm">
<div class="field">
<label for="emailAddress">
Enter your email address:
</label>
<input type="email"
name="emailAddress"
ng-model="data.email"
ng-minlength="5"
ng-maxlength="30"
required>
</form>
Alright nice. Now we can reuse the error-messages template as the basis for our error messages for other input models (input elements) across our form. But now our error message code looks like it was put together by a robot. Everything is general. There is no love. A true form experience is when each error is specific to each piece of data that is collected in the form. Let's add back our required message and not use the one defined in the template. But hold on a sec! How exactly can one message be used and not the rest? Can we just place the same directive back inside of the ng-messages container? Yes. And by doing so the message defined in the template will be replaced by whatever directive is present within the container.
<div ng-messages="userForm.emailAddress.$error" ng-messages-include="error-messages">
<div ng-message="required">
You did not enter an email address
</div>
</div>
The order of the messages is still the same (with the required message showing up first), but instead of having the general, robot-like required message show up, the message defined within the ng-messages directive will be used instead. This way we can pick and choose which messages are reused and which ones are overwritten.
to topCustom validators can be created by creating a directive, including the ngModel controller and then adding a validation to the $parsers array (the $formatters array can also be used, but $parsers are better suited for validations). Let's create a validator that will check to see if our email address is available within our database via a GET API call.
myApp.directive('recordAvailabilityValidator', ['$http', function($http) {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
var apiUrl = attrs.recordAvailabilityValidator;
function setAsLoading(bool) {
ngModel.$setValidity('recordLoading', !bool);
}
function setAsAvailable(bool) {
ngModel.$setValidity('recordAvailable', bool);
}
ngModel.$parsers.push(function(value) {
if (!value || value.length == 0) return;
setAsLoading(true);
setAsAvailable(false);
$http.get(apiUrl, {
v: value
}).success(function() {
setAsLoading(false);
setAsAvailable(true);
}).error(function() {
setAsLoading(false);
setAsAvailable(false);
});
return value;
})
}
}
}]);
<form name="userForm">
<div class="field">
<label for="emailAddress">
Enter your email address:
</label>
<input type="email"
name="emailAddress"
ng-model="data.email"
ng-minlength="5"
ng-maxlength="30"
record-availability-validator="/api/emails.json"
required />
<div ng-messages="userForm.emailAddress.$error" id="error-messages.html">
<div ng-message="recordLoading">
Checking database...
</div>
<div ng-message="recordAvailable">
The email address is already in use...
</div>
</div>
</div>
<input type="submit" />
</form>
Our validator is nice and reusable. We can use it for checking if an email address is available and we can even use it for other things like validating the uniqueness of usernames or user ID values. Since we have used the ngModel.$setValidity method and provided an error name, that error will show up on the model's $error object whenever the error is present. Therefore, to show two messages (the loading error and the availability error) it is just a matter of placing them as messages inside of the ngMessage directive container.
<div ng-messages="userForm.emailAddress.$error">
<div ng-message="required">
...
</div>
<div ng-message="minlength">
...
</div>
<div ng-message="maxlength">
...
</div>
<div ng-message="email">
...
</div>
<div ng-message="recordLoading">
Checking database...
</div>
<div ng-message="recordAvailable">
The email address is already in use...
</div>
</div>
Now the _recordLoading_and recordAvailable messages are displayed whenever they're being evaluated whenever the email address changes within the model. Now the validator code above isn't perfect--it doesn't check for the existence of other errors nor does it cancel previous XHR requests--but this is an exercise that is up to you to improve upon ;)
If you wish to see an example of this, please view the demo application and enter a username value.
Click here to see the demo application
to topSince the ngMessages and ngMessage directives make use of the $animate service to manage the DOM operations, animations can be placed whenever error messages are swapped and when there are no error messages to display at all. These hooks can allow for some interesting animations to be triggered to the user when an error is presented on screen.
The outer ng-messages directive adds the ng-active CSS class to the container element when there are one or more errors displayed. Otherwise it removes that CSS class and adds the ng-inactive CSS class.
The inner ng-message directives add insert and remove themselves which in triggers the enter and leave animation events to kick off.
Click here to learn about Animations in AngularJS
Click here to learn about animations with ngMessages
to topNgMessages is an experimental module. This doesn't meant that it will be removed one day, but instead it means that the API could change until it is fully stable. Therefore there are any bugs or missing features which you think would be ideal, please create an issue on github and reference @matsko. I would be happy to hear any new ideas!
Thank you for taking the time to read the article.
to top