Using Directive Templates in AngularJS

So far, my custom directive has been generating elements using jqLite or jQuery. This works, but it is essentially an imperative approach to generating declarative content, and for complex projects this mismatch of approaches becomes apparent in complex blocks of jqLite statements that can be hard to read and maintain.

An alternative approach is to generate content from an HTML template, which is used to replace the content of the element to which the directive is applied. In Listing 16-7, you can see how I have created a simple template using the template definition property.

Listing 16-7. Using a Template to Generate Content in the directives.html File

<html ng-app=”exampleApp”>

<head>

<title>Directives</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

scope.data = scope[attrs[“unorderedList”]];

},

restrict: “A”,

template: “<ulxli ng-repeat=’item in data’>”

+ “{{item.price | currency}}</li></ul>”

}

}).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>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h3>Products</h3>

</div>

<div class=”panel-body”>

<div unordered-list=”products”>

This is where the list will go </div>

</div>

</div>

</body>

</html>

The result is a simpler directive. Using code to generate HTML statements in any language can be verbose, even when using a library as tightly focused as jQuery/jqLite. There are two areas of change in the listing. The first is that I create a scope property called data and use it to set the source of the data, which I get from the directive attribute. (To keep the example simple, I changed the restrict definition property to A so that the directive can be applied only as an attribute, which means that I don’t have to check different attribute names to find the source of the data.)

That’s all I have to do in the link function, which is no longer responsible for generating the HTML elements used to present the data to the user. Instead, I have used the template definition property to specify a fragment of HTML that will be used as the content of the element to which the directive has been applied, like this:

template: “<ul><li ng-repeat=’item in data’>{{item.price | currency}}</li></ul>

I concatenated two strings together to create the template in the listing, but that was just so that I could fit the code on the printed page. My HTML fragment consists of an ul element and an li element to which I have applied the ng-repeat directive and used an inline binding expression.

When AngularJS applies the custom directive, it will replace the contents of the div element to which it is applied with the value of the template definition property and then evaluate the new content to look for other AngularJS directives and expressions. The result is that the div element is transformed from this:

<div unordered-list=”products”>

This is where the list will go

</div>

into this:

<div unordered-list=”products”>

<ul><!– ngRepeat: item in data –>

<li ng-repeat=”item in data” class=”ng-scope ng-binding”>$1.20</li>

<li ng-repeat=”item in data” class=”ng-scope ng-binding”>$2.42</li>

<li ng-repeat=”item in data” class=”ng-scope ng-binding”>$2.02</li>

</ul>

</div>

1. Using a Function as a Template

In the previous section, I expressed my template content as a literal string, but the template property can also be used to specify a function that produces templated content. The function is passed two arguments (the element to which the directive has been applied and the set of attributes) and returns the fragment of HTML that will be inserted into the document.

I find this feature useful for separating out my template content from the rest of the directive. In Listing 16-8, you can see how I have created a script element that contains my template and the way that I use a function assigned to the template property to obtain that content for the directive.

Listing 16-8. Separating Template Content in the directives.html File

<head>

<title>Directives</title>

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

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

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

<script type=”text/template” id=”listTemplate”>

<ul>

<li ng-repeat=”item in data”>{{item.price \ currency}}</li>

</ul>

</script>

<script>

angular.module(“exampleApp”, [])

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

scope.data = scope[attrs[“unorderedList”]];

},

restrict: “A”,

template: function () {

return angular.element(

document.querySelector(“#listTemplate”)).html();

}

}

}).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>

</head>

I added a script element that contains the template content I want to use and set the template definition object function. jqLite doesn’t support selecting elements by their id attribute (and I don’t want to use the full jQuery library for such a simple directive), so I have used the DOM API to locate the script element and wrap it in a jqLite object, like this:

return angular.element(document.querySelector(“#listTemplate”)).html();

I use the jqLite html method to get the HTML content of the template element and return it as the result from the template function. I’d rather not break into the DOM API like this, but I find it the least bad option when I want to go outside of the features that jqLite provides for such simple tasks.

Tip You can also get the contents of an element using just the DOM. You can see examples of this in Chapter 17.

2. Using an External Template

Using a script element is a useful way of separating out the template content, but the elements remain part of the HTML document, and this can be hard to manage in complex projects when you want to share templates freely between different parts of the application or even between applications. An alternative approach is to define the template content in a separate file and then use the templateUrl definition object property to specify the file name. In Listing 16-9, you can see the contents of a new HTML file called itemTemplate.html that I added to the angularjs folder.

Listing 16-9. The Contents of the itemTemplate.html File

<p>This is the list from the template file</p>

<ul>

<li ng-repeat=”item in data”>{{item.price | currency}}</li>

</ul>

The file contains the same simple template I used in previous examples, with some additional text to make the source of the content clear. In Listing 16-10, I have set the templateUrl definition property to reference the file.

Listing 16-10. Specifying an External Template File in the directives.html File

<script>

angular.module(“exampleApp”, [])

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

scope.data = scope[attrs[“unorderedList”]];

},

restrict: “A”,

templateUrl: “itemTemplate.html”

}

}).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>

3. Selecting an External Template with a Function

The templateUrl property can be set as a function that specifies the URL that is used by the directive, which provides the means to dynamically select the template based on the element to which the directive has been applied. To demonstrate how this works, I have added a new HTML file called tableTemplate.html in the angularjs folder, the contents of which are shown in Listing 16-11.

Listing 16-11. The Contents of the tableTemplate.html File

<table>

<thead>

<tr><th>Name</th><th>Price</th></tr>

</thead>

<tbody>

<tr ng-repeat=”item in data”>

<td>{{item.name}}</td>

<td>{{item.price | currency}}</td>

</tr>

</tbody>

</table>

This template is based around a table element to make it easy to tell which template file is used to generate content. In Listing 16-12, you can see how I have used a function for the templateUrl property to select the template based on an attribute defined on the element to which the directive is applied.

Listing 16-12. Dynamically Selecting a Template File in the directives.html File

<html ng-app=”exampleApp”>

<head>

<title>Directives</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

scope.data = scope[attrs[“unorderedList”]];

},

restrict: “A”,

templateUrl: function (elem, attrs) {

return attrs[“template”] == “table” ?

“tableTemplate.html” : “itemTemplate.html”;

}

}

}).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>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h3>Products</h3>

</div>

<div class=”panel-body”>

<div unordered-list=”products”>

This is where the list will go </div>

</div>

<div class=”panel-body”>

<div unordered-list=”products” template=”table”>

This is where the list will go </div>

</div>

</div>

<div class=”panel-body”> 

<div unordered-list=”products” template=”table”>

This is where the list will go 

</div>

</div> 

</div>

</body>

</html>

The function assigned to the templateUrl property is passed a jqLite object that represents the element to which the directive has been applied and the set of attributes defined on that element. I check for a template attribute, and if it is present and set to table, I return the URL of the tableTemplate.html file. I return the URL of the itemTemplate.html file if there is no template attribute or if it has any other value. In the body section of the directives.html file, I apply the directive to two div elements, one of which has the attribute and value that I check for. Figure 16-1 shows the result.

4. Replacing the Element

By default, the contents of the template are inserted within the element to which the directive has been applied. You can see this in the previous example, where the ul element is added as a child to the div element. The replace definition property can be used to change this behavior such that the template replaces the element. Before I demonstrate the effect of the replace property, I have simplified the directive and added some CSS styling so that I can emphasize an important effect. Listing 16-13 shows the revised directives.html file.

Listing 16-13. Preparing for the replace Property in the directives.html File

<html ng-app=”exampleApp”>

<head>

<title>Directives</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

scope.data = scope[attrs[“unorderedList”]];

},

restrict: “A”,

templateUrl: “tableTemplate.html”

}

}).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>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h3>Products</h3>

</div>

<div class=”panel-body”>

<div unordered-list=”products” class=”table table-striped”>

This is where the list will go

</div>

</div>

</div>

</body>

</html>

I changed the templateUrl property so that the tableTemplate.html file is always used and added a class attribute to the div element to which I applied the directive. I added the div element to two bootstrap classes: table and table-striped. You can see the effect in Figure 16-2.

The table class has worked because Bootstrap defines it in such a way that it doesn’t need to be applied directly to a table element—but that’s not the case for the table-striped class, so my table lacks contrasting color rows. Here is the first part of the HTML that the directive generated:

<div class=”panel-body”>

<div unordered-list=”products” class=”table table-striped”> <table>

<thead>

<tr><th>Name</th><th>Price</th></tr>

</thead>

In Listing 16-14, you can see how I have applied the replace property.

Listing 16-14. Applying the replace Property in the directives.html File

.directive(“unorderedList”, function () {

return {

link: function (scope, element, attrs) {

scope.data = scope[attrs[“unorderedList”]];

},

restrict: “A”,

templateUrl: “tableTemplate.html”,

replace: true

}

The effect of setting the replace property to true is that the template content replaces the div element to which the directive has been applied. Here is the first part of the HTML that the directive generates:

<div class=”panel-body”>

<table unordered-list=”products” class=”table table-striped”>

<thead>

<tr><th>Name</th><th>Price</th></tr>

</thead>

The replace property doesn’t just replace the element with the template; it also transfers the attributes from the element to the template content. In this case, this means the table and table-striped Bootstrap classes are applied to the table element, creating the result shown in Figure 16-3.

This is a useful technique that allows the content a directive generates to be configured by the context in which the directive is applied. I can use my custom directive in different parts of the application and apply different Bootstrap styles to each table, for example.

You can also use this feature to transfer other AngularJS directives directly to the template content of a directive. In Listing 16-15, you can see how I have applied the ng-repeat directive to the div element in the example.

Listing 16-15. Using the replace Definition Property to Transfer Directives in the directives.html File

<div class=”panel-body”>

<div unordered-list=”products” class=”table table-striped”

ng-repeat=”count in [1, 2, 3]”>

This is where the list will go

</div>

</div>

This has the same effect as applying the ng-repeat directive to the table element in the template file, without needing to reproduce the containing div element.

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 *