Your privacy matters: This site uses cookies to analyze site usage and provide social media features. Learn More.

Understanding Transclusion in AngularJS (Part 1 of 2)

February 03, 2015 in JavaScript Articles

Written by Eric Greene


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.


Written by Eric Greene

Eric Greene
Eric is a professional software developer specializing in HTML, CSS, and JavaScript technologies. He has been developing software and delivering training classes for nearly 19 years. He holds the MCSD Certification for ASP.Net Web Applications, and is a Microsoft Certified Trainer.


Contact Us:

Accelebrate’s training classes are available for private groups of 3 or more people at your site or online anywhere worldwide.

Don't settle for a "one size fits all" public class! Have Accelebrate deliver exactly the training you want, privately at your site or online, for less than the cost of a public class.

For pricing and to learn more, please contact us.

Contact Us

Toll-free in US/Canada:
877 849 1850
International:
+1 678 648 3113

Toll-free in US/Canada:
866 566 1228
International:
+1 404 420 2491

925B Peachtree Street, NE
PMB 378
Atlanta, GA 30309-3918
USA

Subscribe to our Newsletter:

Never miss the latest news and information from Accelebrate:

Microsoft Gold Partner

Please see our complete list of
Microsoft Official Courses

Recent Training Locations

Alabama

Huntsville

Montgomery

Birmingham

Alaska

Anchorage

Arizona

Phoenix

Tucson

Arkansas

Fayetteville

Little Rock

California

San Francisco

Oakland

San Jose

Orange County

Los Angeles

Sacramento

San Diego

Colorado

Denver

Boulder

Colorado Springs

Connecticut

Hartford

DC

Washington

Florida

Fort Lauderdale

Miami

Jacksonville

Orlando

Saint Petersburg

Tampa

Georgia

Atlanta

Augusta

Savannah

Idaho

Boise

Illinois

Chicago

Indiana

Indianapolis

Iowa

Ceder Rapids

Des Moines

Kansas

Wichita

Kentucky

Lexington

Louisville

Louisiana

Banton Rouge

New Orleans

Maine

Portland

Maryland

Annapolis

Baltimore

Hagerstown

Frederick

Massachusetts

Springfield

Boston

Cambridge

Michigan

Ann Arbor

Detroit

Grand Rapids

Minnesota

Saint Paul

Minneapolis

Mississippi

Jackson

Missouri

Kansas City

St. Louis

Nebraska

Lincoln

Omaha

Nevada

Reno

Las Vegas

New Jersey

Princeton

New Mexico

Albuquerque

New York

Buffalo

Albany

White Plains

New York City

North Carolina

Charlotte

Durham

Raleigh

Ohio

Canton

Akron

Cincinnati

Cleveland

Columbus

Dayton

Oklahoma

Tulsa

Oklahoma City

Oregon

Portland

Pennsylvania

Pittsburgh

Philadelphia

Rhode Island

Providence

South Carolina

Columbia

Charleston

Spartanburg

Greenville

Tennessee

Memphis

Nashville

Knoxville

Texas

Dallas

El Paso

Houston

San Antonio

Austin

Utah

Salt Lake City

Virginia

Richmond

Alexandria

Arlington

Washington

Tacoma

Seattle

West Virginia

Charleston

Wisconsin

Madison

Milwaukee

Alberta

Edmonton

Calgary

British Columbia

Vancouver

Nova Scotia

Halifax

Ontario

Ottawa

Toronto

Quebec

Montreal

Puerto Rico

San Juan

© 2013-2019 Accelebrate, Inc. All Rights Reserved. All trademarks are owned by their respective owners.