Defining Complex Directives with AngularJS

In Chapter 16, I showed you how to create custom directives by using a factory function that returns a link function. This is the simplest approach, but it means that defaults are used for many of the options that directives can define.

To customize those options, the factory function must return a definition object, which is a JavaScript object that defines some or all of the properties described in Table 16-2. In the sections that follow, I show you how to apply some of these properties to take control of the way your custom directives are used; the others I describe in Chapter 17.

1. Defining How the Directive Can Be Applied

When you return just a link function, you create a directive that can be applied only as an attribute. This is how most AngularJS directives are applied, but you can use the restrict property to change the default and create directives that can be applied in different ways. In Listing 16-2, you can see how I have updated the unorderedList directive so that it is defined with a definition object and uses the restrict property.

Listing 16-2. Setting the restrict Option in the directives.html File

<script>

angular.module(“exampleApp”, [])

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

var data = scope[attrs[“unorderedList”] || attrs[“listSource”]];

var propertyExpression = attrs[“listProperty”] || “price | currency”;

if (angular.isArray(data)) {

var listElem = angular.element(“<ul>”);

if (element[0].nodeName == “#comment”) {

element.parent().append(listElem);

} else {

element.append(listElem);

}

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

var itemElement = angular.element(“<li>”)

.text(scope.$eval(propertyExpression, data[i]));

listElem.append(itemElement);

}

}

},

restrict: “EACM”

}

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

$scope.products = [

{ name: “Apples”, category: “Fruit”, price: 1.20, expiry: 10 },

{ name: “Bananas”, category: “Fruit”, price: 2.42, expiry: 7 },

{ name: “Pears”, category: “Fruit”, price: 2.02, expiry: 6 }

];

})

</script>

I have changed the factory function method so that it returns an object, which is my definition object, rather than just the link function. I still need a link function for my directive, of course, so I assign the function to the link property of the definition object, as described in Table 16-2. The next change is to add the restrict property to the definition object. This tells AngularJS which of the four ways that I want to allow my custom directive to be used, with each kind of use represented by one of the letters described in Table 16-3.

I specified all four letters in Listing 16-2, which means that my custom directive can be applied in all four ways: as an element, as an attribute, as a class, and as a comment. You can see the directive applied in all four ways in the sections that follow.

Tip It is rare that a directive in a real project would be applicable in all four ways. The most common values for the restrict definition property are A (the directive can be applied only as an attribute), E (the directive can be applied only as an element), or ae (the directive can be applied as an element or an attribute). As I explain in the following sections, the C and m options are rarely used.

1.1. Applying the Directive as an Element

The AngularJS convention is to use elements for directives that manage a template through the template and templateUrl definition properties and that I describe in the “Using Directive Templates” section. That’s just a convention, however, and you can apply any custom directive as an element by including the letter E in the value for the restrict definition property. In Listing 16-3, you can see how I have applied my example directive as an element.

Listing 16-3. Applying a Directive as an Element in the directives.html File

<div class=”panel-body”>

<unordered-list list-source=”products” list-property=”price | currency” />

</div>

I apply the directive as an unordered-list element, which I configure using attributes. This requires me to make a change to the link function for the directive because the source of the data has to be defined with a new attribute.

I have selected the name list-source for the new attribute, and you can see how I check for the value of the attribute if there is no unordered-list attribute value available:

var data = scope[attrs[“unorderedList”] || attrs[“listSource”]];

1.2. Applying the Directive as an Attribute

The AngularJS convention is to apply most directives as attributes, which is why this is the approach that I demonstrated in Chapter 15. But for completeness, Listing 16-4 shows how I apply the custom directive as an attribute.

Listing 16-4. Applying a Directive as an Attribute in the directives.html File

<div class=”panel-body”>

<div unordered-list=”products” list-property=”price | currency”></div>

</div>

No changes were needed to the link function to support applying the directive in this way, of course, since I wrote the original code with this approach in mind.

1.3. Applying the Directive as a Class Attribute Value

Wherever possible, you should apply directives as elements or attributes, not least because these approaches make it easy to see where the directives have been applied. You can, however, also apply directives as the value for the class attribute, which can be helpful when you are trying to integrate AngularJS into HTML that is generated by an application that can’t easily be changed. Listing 16-5 shows how I applied the directive using the class attribute.

Listing 16-5. Applying a Directive as a class Attribute Value in the directives.html File

<div class=”panel-body”>

<div class=”unordered-list: products” list-property=”price | currency”></div>

</div>

I set the value of the class attribute to the directive name. I want to supply a configuration value for the directive, so I follow the name with a colon (the : character) and the value. AngularJS will present this information as though there were an unordered-list attribute on the element, just as though I had applied the directive as an attribute.

I have cheated slightly in this example and defined a list-property attribute on the element to which the directive has been applied. Of course, if I were able to do that in a real project, then I wouldn’t need to apply the directive through the class attribute in the first place. In a real project, I would have to do something like this:

<div class=”panel-body”>

<div class=”unordered-list: products, price | currency”></div>

</div>

This would cause AngularJS to provide the directive link function with a value for the unorderedList attribute of “products, price | currency“, and I would then be responsible for parsing the value in the link function. I have skipped over this because I want to remain focused on AngularJS, rather than on JavaScript string parsing for a feature that I recommend you avoid when possible.

1.4. Applying the Directive as a Comment

The final option is to apply the directive as an HTML comment. This is the last resort, and you should try to use one of the other options whenever possible. Using comments to apply a directive makes it harder for other developers to read the HTML, who won’t be expecting comments to have an effect on application functionality. It can also cause problems with build tools that strip out comments to reduce file size for deployment. In Listing 16-6, you can see how I applied the custom directive as a comment.

Listing 16-6. Applying a Directive as a Comment in the directives.html File

<div class=”panel-body”>

<!– directive: unordered-list products –>

</div>

The comment must start with the word directive, followed by a colon, the name of the directive, and an optional configuration argument. Just as in the previous section, I don’t want to get sucked into the world of string parsing, so I have used the optional argument to specify the source of the data and updated the link function to set a default for the property expression, like this:

var propertyExpression = attrs[“listProperty”] || “price | currency”;

I had to change the way that the link function operates to support the comment approach. For the other approaches, I add the content to the element that the directive has been applied to, but that won’t work for a comment. Instead, I use jqLite to locate and operate on the parent of comment elements, like this:

if (element[o].nodeName == “#comment”) {

element.parent().append(listElem);

} else {

element.append(listElem);

}

This code is a bit of a hack and relies on the fact that jQuery/jqLite objects are presented as an array of HTMLElement objects, which are the browser’s DOM representations of HTML elements. I get the first element in the jqLite object using an array index of zero and call the nodeName property, which tells me what kind of element the directive has been applied to. If it is a comment, then I use the jqLite parent method to get the element that contains the comment and add my ul element to it. This is a pretty ugly approach and is another reason why using comments to apply directives should be avoided.

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 *