Creating Custom Directives with AngularJS: Working with jqLite

Now that I have shown you how to create a custom directive, I am going to step back and show you jqLite, which is the cut-down version of jQuery that AngularJS comes with and that is used within directives to create, manipulate, and manage HTML elements. In this part of this chapter, I will describe the methods that jqLite provides and demonstrate the most important.

I don’t demonstrate every method that jqLite provides because the implementation of each method corresponds to a jQuery method of the same name. See http://jquery.com for the jQuery API documentation, or see my book Pro jQuery 2.0, which is also published by Apress.

1. Navigating the Document Object Models

The first area that I am going to describe is the jqLite support for locating elements in the Document Object Model (DOM). You usually won’t need to navigate around the DOM for simple directives because the link function is passed the element argument, which is the jqLite object that represents the element to which the directive has been applied. You may have to manage a set of elements in more complex directives, and that can require the ability to traverse the element hierarchy to locate and select one or more elements on which to operate. Table 15-3 describes the jqLite methods that deal with DOM navigation.

These methods and their descriptions may appear a little odd if you have not used jQuery. The object that AngularJS uses to represent HTML elements, which I will call the jqLite object, can actually represent zero, one, or multiple HTML elements. That’s why some of the jqLite methods, such as eq, treat the jqLite object as a collection or—like children—return a collection of elements. To give you a sense of how this works, I have added a new HTML file called jqlite.html to the angularjs folder and used it to define a directive that uses jqLite to perform some simple DOM navigation. You can see the contents of the new file in Listing 15-12.

Listing 15-12. The Contents of the jqlite.html File

<html ng-app=”exampleApp”>

<head>

<title>Directives</title>

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

<script>

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

var items = element.children();

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

if (items.eq(i).text() == “Oranges”) {

items.eq(i).css(“font-weight”, “bold”);

}

}

}

})

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

// controller defines no data or behaviors

})

</script>

</head>

<body ng-controller=”defaultCtrl”>

<h3>Fruit</h3>

<ol demo-directive>

<li>Apples</li>

<li>Oranges</li>

<li>Pears</li>

</ol>

</body>

</html>

The directive in this example is called, obviously enough, demoDirective, and it processes the children of the element it is applied to, looking for any element that contains Oranges as its content. This isn’t something you often need to do in a real project, but it lets me demonstrate the basics of using jqLite.

The starting point for this—and for all—directives is the element argument that is passed to the link function. The element argument is a jqLite object that supports all of the methods in Table 15-3 and the other sections in this part of the chapter. The element object represents the element to which the directive has been applied. I start by calling the children method, like this:

var items = element.children();

The result returned by the children method is another jqLite object, but this one contains all of the child elements defined by the element to which the directive has been applied. Child elements are immediate descendants of an element, which in this example is the set of li elements.

I use a standard for loop to enumerate the elements that the items object contains, using the length property to figure out how many there are. For each element, I use the text method, which returns the text content of an element (as I describe in the “Modifying Elements” section) to look for the term Oranges:

if (items.eq(i).text() == “Oranges”) {

Notice that I get the element at the current index using the eq method and not by treating the jqLite object as a JavaScript array (i.e., items[i]). The eq method returns a jqLite object that contains the element at the specified index and that supports all of the jqLite methods. Using a JavaScript array index returns an HTMLElement object, which is what the browser uses to represent elements in the DOM. You can work directly with HTMLElement objects if you want, but they don’t support the jqLite methods, and the DOM API is pretty verbose and painful to work with compared to jqLite/jQuery.

To complete my explanation of this example, I use the css method (which sets a CSS property directly on an element, as I explain in the “Modifying Elements” section) so that the browser displays the text of the element in bold, like this:

items.eq(i).css(“font-weight”, “bold”);

Once again, notice that I access the element I want via the eq method. Figure 15-5 shows the effect of the directive.

1.1. Locating Descendants

The children method returns all of the elements that are directly the element or elements represented by the jqLite object. If you want to go further down the hierarchy of elements, then you need to use the find method, which will look for elements of a specified type in the children, the children’s children, and so on, through all the descendants of an element. You can see the limitation of the children method if I add some additional elements to the list in the jqlite.html file, as shown in Listing 15-13.

Listing 15-13. Adding Elements to the jqlite.html File

<ol demo-directive>

<li>Apples</li>

<ul>

<li>Bananas</li>

<li>Cherries</li>

<li>Oranges</li>

</ul>

<li>Oranges</li>

<li>Pears</li>

</ol>

The children method will return the direct descendants of the ol element only, which includes the newly added ul element but excludes the new li elements. You can see the problem this causes in Figure 15-6 where only one of the elements that contains Oranges has been marked in bold.

By contrast, I can use the find method to locate all of the li elements that are descendants of the ol element, including those further down the hierarchy. You can see how I have applied the find element in Listing 15-14.

Listing 15-14. Applying the find Element in the jqlite.html File

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

vax items = element.find(“li”);

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

if (items.eq(i).text() == “Oranges”) {

items.eq(i).css(“font-weight”, “bold”);

}

}

}

})

It isn’t just that jqLite implements a subset of the methods supported by the full jQuery library; the methods that are supported often have a subset of the features that jQuery provides. This includes the find method, which can locate elements based only on their tag name. The full jQuery implementation of the find method can locate descendants in a range of flexible ways. In this listing, I have specified that the find method should locate all of the li elements that are descendants of the ol element, and this will include the ones that I added in Listing 15-13. You can see the effect of this change in Figure 15-7.

2. Modifying Elements

One of the most common reasons for navigating the DOM in a directive link function is so you can modify one or more elements. jqLite provides a set of methods for modifying the content and attributes of elements, as described in Table 15-4.

Some of these methods come in two forms, and you can use them to get a value or set a value. The form of the method with the fewest arguments will get a value from the first element represented by the jqLite object. So, for example, if you call the css method with one argument, you will receive the value of the property you specified from the first element in the jqLite object—all of the other elements will be ignored. As a demonstration, I have called the css method on the jqLite object returned by the find method in the example directive in the jqlite.html file, as shown in Listing 15-15.

Listing 15-15. Calling the get Version of a jqLite Method in the jqlite.html File

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

var items = element.find(“li”);

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

if (items.eq(i).text() == “Oranges”) {

items.eq(i).css(“font-weight”, “bold”);

} else {

items.eq(i).css(“font-weight”, “normal”);

}

}

console.log(“Element count: ” + items.length);

console.log(“Font: ” + items.css(“font-weight”));

}

})

Tip An exception to this pattern is the text method, which, when called without arguments, returns a string that concatenates the text content of all the elements represented by the jqLite object and not just the first one.

I have written the number of elements represented by the item object and the result of calling the css method with one argument to the JavaScript console. I specified the font-weight property, which produced the following result:

Element count: 6

Font: normal

The result displays the font weight of the first element only, which is normal in this example. By contrast, when you use the method to set a value, it applies to all of the elements that the jqLite object represents. You can see a demonstration of this in Listing 15-16, where I have used the css method to set the CSS color property.

Listing 15-16. Calling the set Version of a jqLite Method in the jqlite.html File

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

var items = element.find(“li”);

items.css(“color”, “red”)}

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

if (items.eq(i).text() == “Oranges”) {

items.eq(i).css(“font-weight”, “bold”);

}

}

}

})

This has the effect of changing the color property for all the li elements that are descendants of the directive element, as shown in Figure 15-8.

Tip If you want to target specific elements, then use the DOM navigation methods, described in Table 15-3, to create a jqLite object that represents just the elements you want to modify.

3. Creating and Removing Elements

You won’t always want to locate and modify existing elements in your directive, of course. You will often want to create new content or remove outdated content from the DOM. In Table 15-5, I have described the methods that jqLite provides for creating and removing elements.

Those methods that receive elements as argument can process jqLite objects or fragments of HTML, which makes it easy to create new content dynamically. The angular.element method bridges the gap between these two approaches and takes an HTML fragment or an HTMLElement object from the DOM and packages it as a jqLite object.

The main issue to watch out for here is that the jQuery fluent API means that many of the methods return a jqLite object containing the original set of elements that were in the jqLite object on which the method was called and not those in the argument. I know that sentence is hard to parse, so I’ll demonstrate the trap that awaits the unwary. In Listing 15-17, you can see that I have updated the jqlite.html file so that the example directive generates a set of list elements.

Listing 15-17. Generating List Elements in the jqlite.html File

<html ng-app=”exampleApp”>

<head>

<title>Directives</title>

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

<script>

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

var listElem = element.append(“<ol>”);

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

listElem.append(“<li>”).append(“<span>”).text(scope.names[i])j

}

}

})

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

$scope.names = [“Apples”, “Bananas”, “Oranges”];

})

</script>

</head>

<body ng-controller=”defaultCtrl”>

<h3>Fruit</h3>

<div demo-directive></div>

</body>

</html>

I have changed the element to which the directive is applied to a div, updated the controller so that it defines a data array in the scope, and revised the directive link function so that it uses jqLite to create an ol element that contains a set of li elements, each of which contains a span element that, in turn, contains a value from the array. The set of elements I want to generate looks like this:

<div demo-directive=””>Oranges</div>

<ol>

<li><span>Apples</span></li>

<li><span>Bananas</span></li>

<li><span>Oranges</span></li>

</ol>

</div>

This isn’t what the example really produces, however, and you can see the real outcome in Figure 15-9.

Using the F12 tools to look at the HTML elements in the DOM shows that the HTML I generated looks like this:

<div demo-directive=””>Oranges</div>Oranges</div>

What went wrong? The answer is that I am operating on the wrong elements in the DOM, right from the start. Here is the first jqLite operation in the example:

var listElem = element.append(“<ol>”);

I append an ol element as a child of the element argument passed to the link function, which represents the div element. The problem is signaled in the name I use in the variable to which I assign the result of the append operation: listElem. In fact, the append method—like all the methods that take element arguments in Table 15-5—returns a jqLite object representing the elements on which the operation was performed, which is, in this example, the div element and not the ol element. That means the other jqLite statement in the example has an unexpected effect:

listElem.append(“<li>”).append(“<span>”).text(scope.names[i]);

There are three operations in this statement—two calls to the append method and a call to the text method— and all of these operations are being applied to the div element. First, I add a new li element as a child of the div element; then, I add a span element. Finally, I call the text method, which has the effect of replacing all the child elements I added to the div with a text string, and since I am performing these operations in a for loop, I repeat them for each value in the array. This is why the div element ends up containing Oranges; it is the last value in the array.

This is an incredibly common mistake to make, even for developers experienced in jQuery. I do it all the time, including when I sketched out the custom directive that I started the chapter with. You must keep an eye on which set of elements you are performing operations on—something that jqLite makes harder than jQuery by omitting some methods that are helpful in keeping track of what’s going on.

I find that the most reliable way of avoiding this problem is to use the angular.element method to create jqLite objects and perform operations on them in separate statements. You can see how I have done this in Listing 15-18, in which I demonstrate how to properly generate the list elements.

Listing 15-18. Fixing the Problem in the jqlite.html File

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

vax listElem = angular.element(“<ol>”);

element.append(listElem);

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

listElem.append(angular.element(“<li>”)

.append(angular.element(“<span>”).text(scope.names[i])));

}

}

})

The effect is the hierarchy of ol, li, and span elements that I described at the start of this section, as shown by Figure 15-10.

4. Handling Events

jqLite includes support for handling events emitted by elements, using the methods described in Table 15-6. These are the same methods that built-in event directives (described in Chapter 11) use to receive and handle events.

In Listing 15-19, you can see how I have added a button element to the markup in the jqlite.html file and use the on method to set up a handler function that is invoked when the element emits the click event.

Listing 15-19. Adding an Event Handler in the jqlite.html File

<html ng-app=”exampleApp”>

<head>

<title>Directives</title>

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

<style>

.bold { font-weight: bold; }

</style>

<script>

angular.module(“exampleApp”, [])

.directive(“demoDirective”, function () {

return function (scope, element, attrs) {

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

element.append(listElem);

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

listElem.append(angular.element(“<li>”)

.append(angular.element(“<span>”).text(scope.names[i])));

}

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

buttons.on(“click”, function (e) {

element.find(“li”).toggleClass(“bold”);

});

}

})

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

$scope.names = [“Apples”, “Bananas”, “Oranges”];

})

</script>

</head>

<body ng-controller=”defaultCtrl”>

<h3>Fruit</h3>

<div demo-directive>

<button>Click Me</button>

</div>

</body>

</html>

Within the directive link function, I use the find method to locate all the button elements that are descendants of the element to which the directive has been applied. I call the on method on the jqLite object that I receive from the find method to register a function as a handler for the click event.

When invoked, the handler function locates the descendant li elements and uses the toggleClass method to add and remove them from the bold class, which corresponds to the simple CSS style I added to the document. The effect is that clicking the button switches the list items between regular and bold text, as shown in Figure 15-11.

5. Other jqLite Methods

There are a few other jQuery methods that jqLite provides that don’t fit into the other categories, and I describe them in Table 15-7. I have listed these methods for completeness, but I do not demonstrate them because they are not widely used in AngularJS directives; see my Pro jQuery 2.0 book for details.

6. Accessing AngularJS Features from jqLite

In addition to the jQuery methods that I described in previous sections, jqLite offers some extra methods that provide access to features that are specific to AngularJS. Table 15-8 describes these methods.

Note You won’t need to use these methods in most projects. I have listed them here for completeness, but they are rarely required.

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 *