Advanced Directive Features in AngularJS: Using Transclusion

The term transclusion means to insert one part of a document into another by reference. In the context of directives, it is useful when you are creating a directive that is a wrapper around arbitrary content. To demonstrate how this works, I have added a new HMTML file called transclude.html to the angularjs folder and used it to define the example application shown in Listing 17-1.

Listing 17-1. The Contents of the transclude.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Transclusion</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=”template”>

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h4>This is the panel</h4>

</div>

<div class=”panel-body” ng-transclude>

</div>

</div>

</script>

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“panel”, function () { return {

link: function (scope, element, attrs) {

scope.dataSource = “directive”;

},

restrict: “E”,

scope: true,

template: function () {

return angular.element(

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

},

transclude: true

}

})

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

$scope.dataSource = “controller”;

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<panel>

The data value comes from the: {{dataSource}}

</panel>

</body>

</html>

My goal in this example is to create a directive that can be applied to arbitrary content in order to wrap it in a set of elements that are styled as a Bootstrap panel. I have called my directive panel and used the restrict definition property to specify that it can be applied only as an element (this isn’t a requirement for using transclusion but rather a convention I use when I write directives that wrap other content). I want to take content like this:

<panel>

The data value comes from the: {{dataSource}}

</panel>

and generate markup like this:

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h4>This is the panel</h4>

</div>

<div class=”panel-body”>

The data value comes from the: controller

</div>

</div>

The term transclusion is used because the content that is inside the panel element will be inserted into the template. Two specific steps are required to apply transclusion. The first is to set the transclude definition property to true when creating the directive, like this:

transclude: true

The second step is to apply the ng-transclude directive in the template at the point where you want the wrapped elements inserted.

Tip Setting transclude to true will wrap the contents of the element to which the directive has been applied, but not the element itself. If you want to include the element, then set the translude property to ‘element’. You can see a demonstration of this in the “Using Compile Functions” section.

I want the elements inserted into the one template div element that is styled as the panel body, like this:

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h4>This is the panel</h4>

</div>

<div class=”panel-body” ng-transclude>

</div>

</div>

Any content that is contained by the panel element will be inserted into the highlighted div element, and you can see the result in Figure 17-1.

You will notice that I included an inline data binding in the content that I transclude:

The data value comes from the: {{dataSource}}

I did this to show an important aspect of the transclusion feature, which is that expressions in the transcluded content are evaluated in the controller’s scope, not the scope of the directive. I defined values for the dataSource property in the controller factory function and the directive link function, but AngularJS has done the sensible thing and taken the value from the controller. I say sensible because this approach means that content that is going to be transcluded doesn’t need to try to work out which scope its data is defined in; you just write expressions as though transclusion were not an issue and let AngularJS work it out.

However, if you do want to take the directive scope into account when evaluating transcluded expressions, then make sure you set the scope property to false, as follows:

restrict: “E”,

scope: false,

template: function () {

This ensures that the directive operates on the controller scope and any values you define in the link function will affect the transcluded expressions. You can see the result of this change in Figure 17-2, which demonstrates that the data value for the inline binding expression is the one defined in the link function.

1. Using Compile Functions

In Chapter 16, I explained that directives that are especially complex or deal with a lot of data can benefit by using a compile function to manipulate the DOM and by leaving the link function to perform other tasks. I rarely use compile functions in my own projects, and I tend to approach performance problems by simplifying my code or optimizing the data I am working with, but in this section I’ll explain how compile functions work.

Aside from performance, there is one advantage to using compile functions, and that is the ability to use transclusion to repeatedly generate contents, much as ng-repeat does. You can see an example in Listing 17-2, which shows the content of the compileFunction.html file I added to the angularjs folder.

Listing 17-2. The Contents of the compileFunction.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Compile Function</title>

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

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

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

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

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

$scope.products = [{ name: “Apples”, price: 1.20 },

{ name: “Bananas”, price: 2.42 }, { name: “Pears”, price: 2.02 }];

$scope.changeData = function () {

$scope.products.push({ name: “Cherries”, price: 4.02 });

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

$scope.products[i].price++;

}

}

})

.directive(“simpleRepeater”, function () {

return {

scope: {

data: “=source”, propName: “@itemName”

},

transclude: ‘element’,

compile: function (element, attrs, transcludeFn) {

return function ($scope, $element, $attr) {

$scope.$watch(“data.length”, function () {

var parent = $element.parent();

parent.children().remove();

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

var childScope = $scope.$new();

childScope[$scope.propName] = $scope.data[i];

transcludeFn(childScope, function (clone) {

parent.append(clone);

});

}

});

}

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl” class=”panel panel-body” >

<table class=”table table-striped”>

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

<tbody>

<tr simple-repeater source=”products” item-name=”item”>

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

</tr>

</tbody>

</table>

<button class=”btn btn-default text” ng-click=”changeData()”>Change</button>

</body>

</html>

This example contains a directive called simpleRepeater that uses transclusion to repeat a set of elements for each object in an array, like a simple version of ng-repeat. The real ng-repeat directive goes to great lengths to avoid adding and removing elements from the DOM, but my example just replaces all of the transcluded elements and so isn’t as efficient. Here is how I applied the directive to an HTML element:

<tbody>

<tr simple-repeater source=”products” item-name=”item”>

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

</tbody>

I specify the source of the data objects using the source attribute and the name that can be used to refer to the current object in the transcluded template using the item-name attribute. For this example, I specified the products array created by the controller and a name of item (which allows me to refer to item.name and item.currency in the transcluded content).

My goal is to repeat the tr element for each product object, so I have set the transclude definition property to element, which means that the element itself will be included in the transclusion, as opposed to its contents. I could have applied my directive to the tbody element and set the transclude property to true, but I wanted to demonstrate both configuration values.

The centerpiece of the directive is the compile function, which is specified using the compile property. The compile function is passed three arguments: the element to which the directive has been applied to, the attributes on that element, and a function that can be used to create copies of the transcluded elements.

The most important thing to realize about compile functions is that they return a link function (the link property is ignored when the compile property is used). This may seem a little odd, but remember that the purpose of a compile function is to modify the DOM, so returning a link function from the compile function can be helpful because it provides an easy way to pass data from one part of the directive to the next.

The compile function is supposed to manipulate the DOM only, so it is not provided with a scope, but a link function returned by a compile function can declare dependencies on $scope, $element, and $attrs arguments, which correspond to their regular link function counterparts.

Don’t worry if this doesn’t make sense; the reason that I used a compile function is solely so I can get a link function that has a scope and can call the transclusion function. As you’ll see, that’s the key combination to creating a directive that can repeat content.

1.1. Understanding the Compile Function

Here is the compile function and—within it—the link function:

compile: function (element, attrs, transcludeFn) {

return function ($scope, $element, $attr) {

$scope.$watch(“data.length”, function () {

var parent = $element.parent();

parent.children().remove();

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

var childScope = $scope.$new();

childScope[$scope.propName] = $scope.data[i];

transcludeFn(childScope, function (clone) {

parent.append(clone);

});

}

});

}

}

The first thing I do in the link function is set up a watcher on the scope for the data.length property so that I can respond when the number of data item changes. I use the $watch method, which I described in Chapter 13. (I don’t have to worry about the individual properties of the data objects since they will be data bound in the transcluded template.)

Within the watcher function I use jqLite to locate the parent of the element to which the directive has been applied and then remove its children. I have to work with the parent element because I set the transclude property to element, which means that I want to add and remove copies of the directive’s element.

The next step is to enumerate the data objects. I create a new scope by calling the $scope.$new method.

This allows me to assign a different object to the item property for each instance of the transcluded content, which I clone like this:

transcludeFn(childScope, function (clone) {

parent.append(clone);

});

This is the most important part of the example. For each data object, I call the transclude function that is passed to the compile function. The first argument is the child scope that contains the item property set to the current data item. The second argument is a function that is passed a cloned set of the transcluded content, which I append to the parent element using jqLite. The result is that I generate a copy of the tr element that my directive has been applied to—and its contents—for each data object and create a new scope that allows the transcluded content to refer to the current data object as item.

So that I can test that the directive responds to changes in the data, I added a Change button that calls the changeData behavior in the controller. This behavior adds a new item to the data array and increments the value of the price property on all of the data objects. You can see the result of my directive and of clicking the Change button in Figure 17-3.

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 *