Custom form elements in Angular

Angular’s magic shines mightily when it comes to HTML forms, as it greatly simplifies form validation etc. But what if you would like to use a custom form element, such as datepicker using a jQuery UI plugin?

Amazingly, Angular’s magic makes this quite easy – if your form element has a name and a ng-model attribute, it will automatically be included in the form handling.

By way of illustration let us assume that we need a re-usable form element for specifying a range. Ranges consist of a minimum and a maximum, and we’d like an input field for both. Clearly the minimum mustn’t be greater than the maximum for the range to be valid.

The HTML for a form with this form element is straightforward.

<!DOCTYPE HTML>

<html ng-app="customFormElementApp">
<head>
    <meta charset="utf-8">
</head>
<body>
<form name="rangeForm" ng-controller="MainCtrl as ctrl">
    <div>
        <label for="price-range">Price range</label>
        <range id="price-range" name="priceRange" ng-model="ctrl.priceRange" required></range>
    </div>
    <div>Selected price range: {{ ctrl.priceRange[0] }} to {{ ctrl.priceRange[1] }}</div>
    <div>This form is valid: {{ rangeForm.$valid }}</div>
    <div><button ng-click="randomRange()">Random range</button></div>
</form>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js"></script>
<script>
    angular.module('customFormElementApp', ['rangeApp'])
            .controller('MainCtrl', ['$scope', function($scope) {
                                var self = this;

                                $scope.randomRange = function() {
                                    var lb = Math.floor(Math.random() * 1000);
                                    var ub = lb + Math.floor(Math.random() * 1000);
                                    self.priceRange = [lb, ub];
                                }
                            }]);
</script>
</body>
</html>

Note that we’ve added a button for generating a random range, so that we can illustrate two-way binding.

Clearly we need a directive for the input element, and the easiest way to handle the range is to use the ngModel controller for the data binding. Chapter 13 of AngularJS: Up and Running has a nice discussion of how to do this. It boils down to code such as the following.

angular.module('rangeApp', [])
        .directive('range', [function() {
                       return {
                           restrict: 'E',
                           require: 'ngModel',
                           template: '<input ng-model="lowerBound" ng-change="updateRange()">'
                           + ' to '
                           + '<input ng-model="upperBound" ng-change="updateRange()">',
                           link: function($scope, $element, $attr, ngModelCtrl) {
                               $scope.updateRange = function() {
                                   var lb = parseFloat($scope.lowerBound);
                                   var ub = parseFloat($scope.upperBound);
                                   var range = null;
                                   if (!isNaN(lb) && !isNaN(ub)) {
                                       range = [lb, ub];
                                   }
                                   ngModelCtrl.$setViewValue(range);
                               };

                               ngModelCtrl.$render = function() {
                                   $scope.lowerBound = ngModelCtrl.$viewValue[0];
                                   $scope.upperBound = ngModelCtrl.$viewValue[1];
                               }
                           }
                       };
                   }]);

If you try out the code, you’ll see that the form is only marked as valid if both text fields are filled in with a number. So is all fine?

Well, not quite. The form is still marked as valid even if the minimum is greater than the maximum. So we must let Angular know what is supposed to be a valid value. This can be achieved with the ngModel controller’s $parsers and $formatters fields.

                               ngModelCtrl.$parsers.unshift(function(value) {
                                   var valid = value instanceof Array && value.length == 2 &&  value[0] <= value[1];
                                   ngModelCtrl.$setValidity('range', valid);
                                   return valid ? value : undefined;
                               });

                               ngModelCtrl.$formatters.unshift(function(value) {
                                   var valid = value instanceof Array && value.length == 2 && value[0] <= value[1];
                                   ngModelCtrl.$setValidity('range', valid);
                               });

The first argument of the $setValidity is the error key Angular will use for the form’s $ewrror field. Again you may refer to chapter 13 of AngularJS: Up and Running for more information.

Read more