US & Canada: 877 849 1850
International: +1 678 648 3113

Accelebrate Blog

ACCELERATED LEARNING, CELEBRATED RESULTS

Understanding Transclusion in AngularJS (Part 1 of 2)

AngularJS is one of the most popular JavaScript client-side frameworks in the marketplace today. Among the many great features of AngularJS is the ability to create directives. At their core, directives are nothing more than markers placed on DOM elements that use JavaScript to manipulate that DOM element and its content. jQuery gurus regularly perform similar actions on DOM elements yet AngularJS’s formulation for a directive provides an opinionated structure and best practice for performing those DOM manipulations.

One of those structures and practices is the concept and process of transclusion. For many a new AngularJS developer, the word transclusion itself is confusing and strikes fear and mystery into the heart and mind. However, the concept of transclusion is really a simple one, even though its practical application can be challenging in certain scenarios. In this post, I will help define transclusion and explain how it works.

The Need for Transclusion

Consider the following HTML fragment and custom directive JavaScript code below.

HTML Code

<div foo>
    Some Content Here
</div>

JavaScript Code

.directive("foo", function() {
  return {
    template: "<div>the template</div>"
  };
})

When this directive is compiled and linked into the DOM by AngularJS, the template will replace the content within the DIV element in the HTML fragment, producing the following output.

HTML Code

<div foo>
  <div>the template</div>
</div>  

To utilize the original DIV content (now replaced by the template), the directive must clone it and add it to the DOM using transclusion. To say it another way, transclusion is simply the method by which a directive displays content that was replaced by a directive’s template.

First Transclusion Method: ngTransclude

In the above example, we have several options to transclude the original content of the DIV element. We can use the ngTransclude directive or we can use the transclude function passed as the fifth parameter to the post-link function. First, let’s demonstrate the ngTransclude directive. Using the same code as above, the directive needs to turn on the transclusion option by setting it to true in the Directive Definition Object, then the directive needs to apply the ngTransclude directive in the template. During the linking process, the ngTransclude directive will clone the DIV element’s original content and add it to the DOM as a child element of the element on which the ngTransclude directive is applied.

HTML Code

<div foo>
  Some Content Here
</div>

JavaScript Code

.directive("foo", function() {
  // returns the Directive Definition Object
  return {
    transclude: true,
    template: "<div>the template</div><div ng-transclude></div>"
  };
})

The result of the using the ngTransclude directive is the following HTML output.

HTML Code

<div foo>
  <div>the template</div>
  <div ng-transclude>Some Content Here</div>
</div>

Observe that the original content of the DIV element has now been loaded into the content area of the DIV element on which the ngTransclude directive was placed. As you can see, transclusion is actually a pretty straightforward concept.

Second Transclusion Method: Transclude Function

The second method to do transclusion is to use the transclude function provided in the post-link function of a directive. In the code below, the link property points to the post-link function. Unless a pre-link function is specifically provided, all references to a link function refer to the post-link function. Review the code below to see how the same goal is achieved with a transclude function:

HTML Code

<div foo>
  Some Content Here
</div>

JavaScript Code

.directive("foo", function() {
  return {
    template: "<div>the template</div>",
    transclude: true,
    link: function(scope, element, attrs, ctrl, transclude) {
      transclude(function(clone) {
        element.append(clone);
      });
    }
  };
})

The transclude function is passed an argument that is a callback function used to manipulate the cloned element. In the code above, the callback function will receive the cloned content as a parameter, then the function adds the cloned content to the DOM. The execution of the of link function results in the following HTML output:

HTML Code

<div foo>
  <div>
      the template
      <div>Some Content Here</div>
  </div>
</div>

While the resulting HTML is slightly different because of how the DOM structure is referenced, the goal of transcluding the content is still achieved.

Making Multiple Clones of the Content

The transclude function is incredibly useful because it can be used to change the scope (we will explore scope and transclusion later in the post) of the transcluded content and it can be called multiple times to duplicate the transcluded content as demonstrated here.

JavaScript Code

.directive("foo", function() {
  return {
    template: "<div>the template</div>",
    transclude:true,
    link: function(scope, element, attrs, ctrl, transclude) {
      transclude(function(clone) {
        element.append(clone);
      });
      transclude(function(clone) {
        element.append(clone);
      });
      transclude(function(clone) {
        element.append(clone);
      });
    }
  };
})

When the foo directive is linked, the updated link function will produce HTML that looks like this.

HTML Code

<div foo>
  <div>
    the template
    <div>Some Content Here</div>
    <div>Some Content Here</div>
    <div>Some Content Here</div>
  </div>
</div>

You may be wondering why the transclude function must be called three times. Why can’t the transclusion code be written like this?

JavaScript Code

link: function(scope, element, attrs, ctrl, transclude) {
  transclude(function(clone) {
    element.append(clone);
    element.append(clone);
    element.append(clone);
  });
}

The reason this does not work is that the same DOM element named clone is being added three times. Three copies are not being added, the same one is being added three times. When passing the clone into element.append, the function uses the memory reference of the DOM element to add it to the DOM. Adding it again does nothing except move the clone to another part of the DOM. When the transclude function is called multiple times, on each call a new clone is made of the original DIV element content and that clone is passed into the transclude function.

Transclusion & Scope

As mentioned earlier, another key aspect of transclusion concerns scope. When content is transcluded, what is the scope of the transcluded content? The answer to this question depends upon the version of AngularJS being used. If your application needs to support Internet Explorer 8, and is therefore using AngularJS 1.2 or earlier, then the scope of the transcluded content will be, by default, a child scope of the scope of the DOM element on which the directive is applied. If your application only needs to support Internet Explorer 9 and above, and is therefore using AngularJS 1.3 or later, then by default, the scope of the transcluded content will be a child scope of the directive’s scope. Because of this difference, a best practice is not to rely upon the default behavior, but rather to specifically set the scope of the transcluded content when calling the transclude function. Review the code sample below:

JavaScript Code

.directive("foo", function() {
  return {
    transclude: true,
    template: "<div>the template</div>",
    link: function(scope, element, attrs, ctrl, transclude) {
      transclude(scope.$new(), function(clone) {
        element.append(clone);
      });
      transclude(scope.$new(), function(clone) {
        element.append(clone);
      });
      transclude(scope.$new(), function(clone) {
        element.append(clone);
      });
    }
  };
})

Observe how a scope argument is now being passed into the transclude function. In this case, the code is specifically setting the scope to be a child scope of the directive’s scope. This code fully works with both AngularJS 1.2 and 1.3, by not relying upon different default behaviors; but instead, by being specific about the scope of the transcluded content. Within the callback function passed to the transclude function, we can access the scope of the clone, by adding a second parameter to the callback function as shown here:

JavaScript Code

transclude(scope.$new(), function(clone, scope) {
  scope.message = "Adding a property to the child scope of the transcluded content";
  element.append(clone);
});

Understanding and utilizing scope with transcluded content is critical to building complex directives.

Transclude the Whole Element not just the Content

The final concept of transclusion that needs to be explored is how to transclude not only the content, but also the actual element (and its content) on which the directive is applied. Consider the common directive ngRepeat as shown in the code below, where colors is an array of string values:

HTML Code

<ul>
  <li ng-repeat="color in colors">{{color}}</li>
</ul>

The resulting output of the ngRepeat directive would be something similar to this.

HTML Code

<ul>
  <!-- ngRepeat: color in colors -->
  <li ng-repeat="color in colors" class="ng-binding ng-scope">red</li>
  <!-- end ngRepeat: color in colors -->
  <li ng-repeat="color in colors" class="ng-binding ng-scope">blue</li>
  <!-- end ngRepeat: color in colors -->
  <li ng-repeat="color in colors" class="ng-binding ng-scope">green</li>
  <!-- end ngRepeat: color in colors -->
  <li ng-repeat="color in colors" class="ng-binding ng-scope">black</li>
  <!-- end ngRepeat: color in colors -->
</ul>

Ignoring the comments (they are added as placeholders in the DOM for ngRepeat’s internal purposes), you will observe that more than the content, {{color}}, was transcluded; but rather, the entire LI element was transcluded. This is another variation on transclusion that allows the element on which the directive is applied to be transcluded as well, thereby permitting it to be easily cloned and added to the DOM multiple times. This variation is called element transclusion and it is configured like this:

JavaScript Code

.directive("foo", function() {
  return {
    transclude: 'element',
    compile: function(tElement, tAttrs) {
      var parentElement = tElement.parent();
      return function(scope, element, attrs, ctrl, transclude) {
        transclude(scope.$new(), function(clone) {
          parentElement.append(clone);
        });
        transclude(scope.$new(), function(clone) {
          parentElement.append(clone);
        });
        transclude(scope.$new(), function(clone) {
          parentElement.append(clone);
        });
      };
    }
  };
})

Observe how the transclude option in the Directive Definition Object is no longer set to true. Instead, it has been set to ‘element’. When performing element transclusion, the template option is ignored. With element transclusion, the DOM element the template would have been a child of is removed because it has been transcluded. Finally, because the element itself is being transcluded, the compile function is used to get a reference to the parent element of the transcluded element so that the cloned elements can be added back to the DOM as children of the parent element.

If a reference to the parent element is not saved, then adding cloned content to the DOM using the element parameter of the link function will do nothing. It does nothing because that element is now a comment placeholder for the original DOM element that was removed because of the transclusion process.

Conclusion

Transclusion is a very powerful and useful feature of AngularJS directives. It allows a directive to use a template while still having the ability to clone the original content and add it to the DOM. Furthermore, as with element transclusion, there are many benefits to transclusion even without a template. Transclusion allows directives to generate dynamic, data-driven DOM structures that create a compelling user experience.

Continue on to part 2 of 2


Author: Eric Greene, one of Accelebrate’s instructors.

Accelebrate offers private AngularJS training and JavaScript training for groups and instructor-led online JavaScript classes for individuals.

Categories: JavaScript Articles
Tags: , , ,

21 Responses to "Understanding Transclusion in AngularJS (Part 1 of 2)"

Leave a Reply

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

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

*



You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Please contact us for GSA pricing.
Contract #GS-35F-0307T

Please see our complete list of
Microsoft Official Courses