Blog

How to implement validation callbacks in AngularJS 1.3

26 Nov, 2014
Xebia Background Header Wave

In my current project we’ve recently switched from AngularJS 1.2 to 1.3. Except for a few breaking changes the upgrade was quite trivial. However, after diving into the changelog we noticed that the way AngularJS handles form validation changed drastically. Since we’re working on a greenfield application we decided it was worth the effort to rewrite the validation logic. The main argument for this was that the validation we had could be drastically simplified by using the new validation pipeline.
This article is aimed at AngularJS developers interested in the new validation pipeline offered by AngularJS 1.3. Except for a small introduction, this article will not be about all the different aspects related to validating forms. I will showcase 2 different cases in which we had to come up with custom solutions:

  • Displaying additional information after successful validation
  • Validating equality of multiple password fields

What has changed?

Whereas in AngularJS 1.2 we could use $parsers and $formatters for form validation, AngularJS 1.3 introduces the concept of $validators and $asyncValidators. As we can deduce from the names, the latter is for server-side validations using HTTP calls and the former is for validations on the client-side.
All validators are directives that are registered on a specific ngModel by either adding it to the ngModel.$validators or ngModel.$asyncValidators. When validating, all $validators run before executing the $asyncValidators.
AngularJS 1.3 also utilises the HTML5 validation API wherever possible. For each of the HTML5 validation attributes, AngularJS offers a directive. For example, for minlength, add ng-minlength to your input field to incorporate the minimum length check.
When it comes down to showing error messages we can rely on the $error property on a ngModel. Whenever one of the validators fail it will add the name of the validator to the $error property. Using the brand new ngMessages module we can then easily display specific error messaging depending on the type of validator.

Displaying additional information after successful validation

Implementing the new validation pipeline came with a few challenges. The biggest being that we had quite a few use cases in which, after successfully validating a field, we wanted to display some data returned by the web service. Below I will discuss how we’ve solved this.
The directive itself is very simple and merely does the following:

  1. Clear the data displayed next to the field. If the user has already entered text and the validation succeeds the data from validation call will be shown next to the input field. If the user were to change the input’s value and it would not validate correctly, the data displayed next to the field would be stale. To prevent this we first clear the data displayed next to the field at the start of the validation.
  2. Validate the content against the web service using the HelloResource. Besides returning the promise the resource gives us we invoke the callback() method when the promise is successfully resolved.
  3. Display data returned by the HTTP call using a callback method

[javascript]
‘use strict’;
angular.module(‘angularValidators’)
.directive(‘validatorWithCallback’, function (HelloResource) {
return {
require: ‘ngModel’,
link: function (scope, element, attrs, ngModel) {
function callback(response) {}
ngModel.$asyncValidators.validateWithCallback = function (modelValue, viewValue) {
callback(”);
var value = modelValue || viewValue;
return HelloResource.get({name: value}).$promise.then(function (response) {
callback(response);
});
};
}
};
});
[/javascript]
We can add the validator to our input by adding the validator-with-callback attribute to the input which we would like to validate.
[html]
<form name="form">
<input type="text" name="name" ng-model="name" required validator-with-callback />
</form>
[/html]

Implementing the clear and callback

Because this directive should be independent from any specific ngModel we have to find a way to pass the ngModel to the directive. To accomplish this we add a value to the validator-with-callback attribute. We also change the ng-model attribute value to name.value. Why this is required will be explained later on. To finish we also add a div that will only display when the form element is valid and we will set it to display the value of name.detail.
[html]
<form name="form">
<input type="text" name="name" ng-model="name.value" required validator-with-callback="name" />
<div ng-if="form.name.$valid">{{name.detail}}</div>
</form>
[/html]
The $eval method from scope can be used to resolve the object using the attribute’s value. Displaying the data won’t work if we simply supply and overwrite any scoped object (f.e. $scope.data). We have to add a scoped object name which contains 2 properties: value and detail. Note: the naming is not important.
We will add a controller to our HTML file which will be responsible for setting the default value for our scoped object name. As shown in our HTML view above, the value property will be used for storing the value of the field. The detail property will be used to store the response from the web service call and display it to the user.
[javascript]
‘use strict’;
angular.module(‘angularValidators’)
.controller(‘ValidationController’, function ($scope) {
$scope.name = {
value: undefined,
detail: ”
};
});
[/javascript]
The last thing is changing the directive implementation to retrieve the target object and implement the clear and callback methods. We do this by retrieving the value from the validator-with-callback attribute by calling scope.$eval(attrs.validatorWithCallback). When we have the target object we can implement the callback method.
[javascript]
‘use strict’;
angular.module(‘angularValidators’)
.directive(‘validatorWithCallback’, function (HelloResource) {
return {
require: ‘ngModel’,
link: function (scope, element, attrs, ngModel) {
var target = scope.$eval(attrs.validatorWithCallback);
function callback(response) {
if (.isUndefined(target)) {
return;
}
target.detail = response.msg;
}
ngModel.$asyncValidators.validateWithCallback = function (modelValue, viewValue) {
…omitted…
};
}
};
});
[/javascript]
Note.isUndefined
is a method from the popular Javascript utility library called Lodash.
This is all that’s needed to create a directive with a callback method. This callback method uses data returned by the web service to populate the property value but it can of course be adjusted to do anything we desire.

Validating equality of multiple password fields

The second example we would like to show is a synchronous validator that validates the values of 2 different fields. The use case we had for this were 2 password fields which required to be equal.

Requirements

  • (In)validate both fields when the user changes the value of either one of them and they are (not) equal
  • The validator successfully validates the field if the second input has not been touched or the value of the second input is empty.
  • Only display 1 error message and only when no other validators (required & min-length) are invalid

Implementation

We start of by creating the 2 different form elements with both a required and a ng-minlength validator. We also add a button to the form to show how enabling/disabling the button depending on the validity of the form works.
Both password fields also have the validate-must-equal-to="other_fieldname" attribute. This indicates that we wish to validate the value of this field against the field defined by the attribute. We also add a form-name="form" attribute to pass in the name of the form to our directive. This is needed for invalidating the second input on our form without hardcoding the form name inside the directive and thus making this directive fully independent from form and field names.
To conclude we also conditionally show or hide the error displaying containers. For the errors related to the first input field we also specify that it should not display if the notEqualTo error has been set by our directive. This ensures that no empty div will be displayed if our validator invalidates the first field.
[html]
<form name="form">
<input type="password" name="password" ng-model="password" required ng-minlength="8" validate-must-equal-to="password2" form-name="form" />
<div ng-messages="form.password.$error" ng-if="form.password.$touched && form.password.$invalid && !form.password.$error.notEqualTo">
<div ng-message="required">This field is required</div>
<div ng-message="minlength">Your password must be at least 8 characters long</div>
</div>
<input type="password" name="password2" ng-model="password2" required ng-minlength="8" validate-must-equal-to="password" form-name="form" />
<div ng-messages="form.password2.$error" ng-if="form.password2.$touched && form.password2.$invalid">
<div ng-message="required">This field is required</div>
<div ng-message="minlength">Your password must be at least 8 characters long</div>
</div>
The submit button will only be enabled when the entire form is valid
<button ng-disabled="form.$invalid">Submit</button>
</form>
[/html]
The validator itself is again very compact. Basically all we want is to retrieve the value from the input and pass it to a isEqualToOther method which returns a boolean. At the beginning of the link method we also do a check to see if the form-name attribute is provided. If not, we throw an error. We do this to communicate to any developer reusing this directive that this directive requires the form name to function correctly. Unfortunately at this moment there is no other way to communicate the additional mandatory attribute.
[javascript]
‘use strict’;
angular.module(‘angularValidators’)
.directive(‘validateMustEqualTo’, function () {
return {
require: ‘ngModel’,
link: function (scope, element, attrs, ngModel) {
if (
.isUndefined(attrs.formName)) {
throw ‘For this directive to function correctly you need to supply the form-name attribute’;
}
function isEqualToOther(value) {
…omitted…
}
ngModel.$validators.notEqualTo = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return isEqualToOther(value);
};
}
};
});
[/javascript]
The isEqualToOther method itself does the following:

  • Retrieve the other input form element
  • Throw an error if it cannot be found which again means this directive won’t function as intended
  • Retrieve the value from the other input and validate the field if the input has not been touched or the value is empty
  • Compare both values
  • Set the validity of the other field depending on the comparison
  • Return the comparison to (in)validate the field this directive is linked to

[javascript]
‘use strict’;
angular.module(‘angularValidators’)
.directive(‘validateMustEqualTo’, function () {
return {
require: ‘ngModel’,
link: function (scope, element, attrs, ngModel) {
…omitted…
function isEqualToOther(value) {
var otherInput = scope[attrs.formName][attrs.validateMustEqualTo];
if (.isUndefined(otherInput)) {
throw ‘Cannot retrieve the second field to compare with from the scope’;
}
var otherValue = otherInput.$modelValue || otherInput.$viewValue;
if (otherInput.$untouched ||
.isEmpty(otherValue)) {
return true;
}
var isEqual = (value === otherValue);
otherInput.$setValidity(‘notEqualTo’, isEqual);
return isEqual;
}
ngModel.$validators.notEqualTo = function (modelValue, viewValue) {
…omitted…
};
}
};
});
[/javascript]

Alternative solution

An alternative solution to the validate-must-equal-to directive could be to implement a directive that encapsulates both password fields and has a scoped function that would handle validation using ng-blur on the fields or a $watch on both properties. However, this approach does not use the out-of-the-box validation pipeline and we would thus have to extend the logic in the form button’s ng-disabled to allow the user to submit the form.

Conclusion

AngularJS 1.3 introduces a new validation pipeline which is incredibly easy to use. However, when faced with more advanced validation rules it becomes clear that certain features (like the callback mechanism) are lacking for which we had to find custom solutions. In this article we’ve shown you 2 different validation cases which extend the standard pipeline.

Demo application

I’ve set up a stand-alone demo application which can be cloned from GitHub. This demo includes both validators and karma tests that cover all different scenario’s. Please feel free to use and modify this code as you feel appropriate.

Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts