Advanced Directive Features in AngularJS: Creating Custom Form Elements

I introduced the ng-model directive in Chapter 10 when I showed you two-way data binding and again in Chapter 12 when I described the way that AngularJS supports HTML forms. The way that the ng-model directive is structured allows you to go beyond the standard form elements and capture data input in any way you want, giving you complete freedom about the components that you create and present to your users. As a demonstration, I added a file called customForms.html to the angularjs folder and used it to create the example shown in Listing 17-6.

Listing 17-6. The Contents of the customForms.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>CustomForms</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”triTemplate”>

<div class=”well”>

<div class=”btn-group”>

<button class=”btn btn-default”>Yes</button>

<button class=”btn btn-default”>No</button>

<button class=”btn btn-default”>Not Sure</button>

</div>

</div>

</script>

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.dataValue = “Not Sure”;

})

.directive(“triButton”, function () {

return {

restrict: “E”,

replace: true,

require: “ngModel”,

template: document.querySelector(“#triTemplate”).outerText,

link: function (scope, element, attrs, ctrl) {

var setSelected = function (value) {

var buttons = element.find(“button”);

buttons.removeClass(“btn-primary”);

for (var i = 0; i < buttons.length; i++) {

if (buttons.eq(i).text() == value) {

buttons.eq(i).addClass(“btn-primary”);

}

}

}

setSelected(scope.dataValue);

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div><tri-button ng-model=”dataValue” /></div>

<div class=”well”>

Value:

<select ng-model=”dataValue”>

<option>Yes</option>

<option>No</option>

<option>Not Sure</option>

</select>

</div>

</body>

</html>

This listing defines the structure of my custom form element but doesn’t yet use the API. I am going to explain how my control will work and then apply the new techniques. As it stands, this example doesn’t contain anything new. I have created a directive called triButton that can be applied as an element and presents the user with three button elements that have been styled using Bootstrap. I have declared a dependency on the ngModel controller (which is the controller defined by the ng-model directive since AngularJS normalizes names), and I have added the ctrl argument to the link function.

I defined a function called setSelected within the link function that I use to highlight the button element that represents the form value that my directive displays. I do this by using jqLite to add and remove a Bootstrap class; you can see the effect in Figure 17-5.

Notice that I applied the ng-model directive to my tri-button element, as follows:

<div><tri-button ng-model=”dataValue” /></div>

This applies the directive to my custom element and sets up a two-way binding to the dataValue property on the scope. My goal is to use the ngModel controller API to implement that binding within my triButton directive.

I included a select element that is bound to the dataValue property as well. This isn’t part of my custom directive, but since I am implementing a two-way data binding, I need to be able to show the effect of the user changing the dataValue value through the custom directive and how to receive and handle notification that the value has been changed elsewhere.

3. Handling External Changes

The first feature I am going to add is the ability to change the highlighted button when the dataValue property is modified outside of my directive, which in this example means through the select element (but could be from any number of sources in a real project). You can see the changes I made to the link function in Listing 17-7.

Listing 17-7. Handling Changes to the Data Value in the customForms.html File

link: function (scope, element, attrs, ctrl) {

var setSelected = function (value) {

var buttons = element.find(“button”);

buttons.removeClass(“btn-primary”);

for (var i = 0; i < buttons.length; i++) {

if (buttons.eq(i).text() == value) {

buttons.eq(i).addClass(“btn-primary”);

}

}

}

ctrl.$render = function () {

setSelected(ctrl.$viewValue || “Not Sure”);

}

}

The change is minor, but it has a big impact. I have replaced the $render function defined by the ngModel controller with one that calls my setSelected function. The $render method is called by the ng-model directive when the value has been modified outside the directive and the display needs to be updated. I get the new value by reading the $viewValue property.

Tip Notice that I removed the explicit call to setSelected that was present in Listing 17-6. The ngModel controller will call the $render function when the application first starts so that you can set the initial state of your directive. The value of the $viewValue will be undefined if you are using dynamically defined properties, and that’s why it is good practice to provide a fallback value, as I have done in the listing.

You can see the effect by loading the customForms.html file into the browser and using the select element to change the value of the dataValue property, as shown in Figure 17-6. Notice that my directive code doesn’t reference the dataValue property directly; the data binding and the data property are managed through the NgModel controller API.

The $render method and the $viewValue properties are the mainstays of the API provided by the NgModel controller, but I have described the complete set of basic methods and properties in Table 17-3. I say basic because there are some others that relate to form validation that I describe in a later section.

I’ll show you how to use the remaining methods and properties in the sections that follow.

4. Handling Internal Changes

The next addition to my custom directive is the ability to propagate changes through the ng-model directive to the scope when the user clicks one of the buttons. You can see how I have done this in Listing 17-8.

Listing 17-8. Adding Support for Propagating Changes in the customForms.html File

link: function (scope, element, attrs, ctrl) {

element.on(“click”, function (event) {

setSelected(event.target.innerText);

scope.$apply(function () {

ctrl.$setViewValue(event.target.innerText);

});

});

var setSelected = function (value) {

var buttons = element.find(“button”);

buttons.removeClass(“btn-primary”);

for (var i = 0; i < buttons.length; i++) {

if (buttons.eq(i).text() == value) {

buttons.eq(i).addClass(“btn-primary”);

}

}

}

ctrl.$render = function () {

setSelected(ctrl.$viewValue || “Not Sure”);

}

}

I have used the jqLite on method, described in Chapter 15, to register a handler function for the click event on the button elements in the directive template. When the user clicks one of the buttons, I notify the NgModel controller by calling the $setViewValue method, like this:

scope.$apply(function () {

ctrl.$setViewValue(event.target.innerText);

});

I introduced the scope.$apply method in Chapter 13 and explained that it is used to push updates into the data model. In Chapter 13, I pass the $apply method an expression for the scope to evaluate, but I have used a function as the argument in this example. The scope will execute the function and then update its state; using a function allows me to notify the NgModel controller of the change and have the scope update its state in a single step.

To update the data-bound value, I call the $setViewValue method, which accepts the new value as its argument. For this example, I get the value from the text content of the button that has been clicked, such that clicking the Yes button causes the dataValue property to be set to Yes.

5. Formatting Data Values

In Table 17-3, I described the $viewValue and $modelValue properties. The NgModel controller provides a simple mechanism for formatting values in the data model so that they can be displayed by a directive. The application of these formatters, which are expressed as functions, transforms the $modelValue property into the $viewValue. Listing 17-9 shows the use of a formatter that maps an additional value defined by the select value to the buttons provided by the directive.

Listing 17-9. Using a Formatter in the customForms.html File

link: function (scope, element, attrs, ctrl) {

ctrl.$formatters.push(function (value) {

return value == “Huh?” ? “Not Sure” : value;

});

// …other statements omitted for brevity…

}

The $formatters property is an array of functions that are applied in order. The result from the previous formatter is passed as the argument, and the function returns its formatted result. The formatter I created in this instance maps a new value, Huh?, to Not Sure. To make use of the formatter, I have added the new value to the select element, as shown in Listing 17-10.

Listing 17-10. Adding a Value to the select Element in the customForms.html File

<div class=”well”>

Value: <select ng-model=”dataValue”>

<option>Yes</option>

<option>No</option>

<option>Not Sure</option>

<option>Huh?</option>

</select>

</div>

You can see the effect in Figure 17-7. The select element is set to Huh?, but my custom directive has highlighted the Not Sure button. The key point to note is that the result of the formatting is assigned to the $viewValue property but that you can get the unformatted value if you need it from the $modelValue property.

6. Validating Custom Form Elements

The ngModel controller also provides support for integrating custom directives into the AngularJS form validation system. To demonstrate how this works, Listing 17-11 shows how I have updated my triButton directive so that only the Yes and No values are valid.

Listing 17-11. Adding Validation in the customForms.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>CustomForms</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<style>

*.error { color: red; font-weight: bold; }

</style>

<script type=”text/ng-template” id=”triTemplate”>

<div class=”well”>

<div class=”btn-group”>

<button class=”btn btn-default”>Yes</button>

<button class=”btn btn-default”>No</button>

<button class=”btn btn-default”>Not Sure</button>

</div>

<span class=”error” ng-show=”myForm.decision.$error.confidence”>

You need to be sure

</span>

</div>

</script>

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.dataValue = “Not Sure”;

})

.directive(“triButton”, function () {

return {

restrict: “E”,

replace: true,

require: “ngModel”,

template: document.querySelector(“#triTemplate”).outerText,

link: function (scope, element, attrs, ctrl) {

var validateParser = function (value) {

var valid = (value == “Yes” || value == “No”);

ctrl.$setValidity(“confidence”, valid);

return valid ? value : undefined;

}

ctrl.$parsers.push(validateParser);

element.on(“click”, function (event) {

setSelected(event.target.innerText);

scope.$apply(function () {

ctrl.$setViewValue(event.target.innerText);

});

});

var setSelected = function (value) {

var buttons = element.find(“button”);

buttons.removeClass(“btn-primary”);

for (var i = 0; i < buttons.length; i++) {

if (buttons.eq(i).text() == value) {

buttons.eq(i).addClass(“btn-primary”);

}

}

}

ctrl.$render = function () {

setSelected(ctrl.$viewValue || “Not Sure”);

}

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<form name=”myForm” novalidate>

<divxtri-button name=”decision” ng-model=”dataValue” /></div>

</form>

</body>

</html>

Most of the changes in this listing are for the standard form validation techniques that I described in Chapter 12.

I have added a span element to the directive template whose visibility is keyed to a validation error called confidence, and I have added a form element to wrap the triButton directive and applied the name attribute.

To perform the validation, I have defined a new function called validateParser, as follows:

var validateParser = function (value) {

var valid = (value == “Yes” || value == “No”);

ctrl.$setValidity(“confidence”, valid);

return valid ? value : undefined;

}

Parser functions are passed the data-bound value and are responsible for checking to see whether it is valid. The validity of a value is set with a call to the $setValidity method defined by the NgModel controller, where the arguments are the key (used to display the validation message) and the validation status (expressed as a Boolean). The parser function is also required to return undefined for invalid values. Parsers are registered by adding the function to the $parsers array, defined by the NgModel controller, as follows:

ctrl.$parsers.push(validateParser);

A directive can have multiple parser functions, just as it can have multiple formatters. You can see the result of the validation by loading the customForms.html file into the browser and clicking the Yes button and then the Not Sure button, as shown in Figure 17-8.

The NgModel controller provides a range of methods and properties that are useful for integrating a custom directive into the validation process, as described in Table 17-4.

You may have wondered why you had to click the Yes button before clicking Not Sure to reveal the validation message. The issue is that validation is not performed until the user interacts with the UI presented by the directive (or more, accurately, when a new value is passed to the NgModel controller), so the parsers are not used until the model changes.

This isn’t always what is required and doesn’t make sense for my example directive, but the problem can be addressed by explicitly calling the parser function in the $render function, as shown in Listing 17-12.

Listing 17-12. Explicitly Calling a Parser Function in the customForms.html File

ctrl.$render = function () {

validateParser(ctrl.$viewValue);

setSelected(ctrl.$viewValue || “Not Sure”);

}

This is a bit of a hack, but it does the job, and the validation message is displayed as soon as the HTML file is loaded.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *