Transclude - That's not a word you'll find in a dictionary :). Once you dive into Angular, creating custom directives is a daily chore and having good understanding of transclusion becomes imperative. To explain it in one sentence, transclusion consists of plucking out the content of a custom directive, processing it against right scope and then placing it at a marked position in the template of that directive. While the basic transclude using the built-in ngTransclude directive is easy enough, there isn't much clarity in some areas in terms of documentation or examples. These areas are -

  • Purpose and usage of transclude argument that a directive compile function receives.
  • Purpose and usage of $transclude injectable in a Controller.
  • Isolate scope and transclude.
  • Transcluding into an attribute.

I'll try to throw some light on these with the help of working fiddles.

Basic transclude

Let's start with the most basic transclude example. Let's say, we are creating a buttonBar directive that a user can use to add a group of buttons to a page. The directive takes care of alignment of the buttons. Just to note, I have used Twitter Bootstrap as CSS framework.

Here is the fiddle for it.


Take a look at the markup. The buttonBar directive wraps a couple of button elements. The button elements also have a class-based directive "primary" attached to them. Don't worry about it though, it simply attaches appropriate Twitter Bootstrap classes to them. The directive provides transclude: true attribute and it's template uses ng-transclude directive on a contained div. At run-time, Angular gets the content of the custom directive, processes it and sticks the resultant markup in the ng-transclude div in the template. And that's all there to it. Quite simple, isn't it?

Transclude at multiple locations

Lets say we would like to enhance our buttonBar directive to have two kinds of buttons - primary and secondary; with primary buttons right-aligned and secondary left-aligned. It would entail picking up the contents(buttons in our example) of the directive and adding them into two separate divs, one for primary buttons and the other for secondary buttons. Transclude into two different locations, if you will. The result should look something like this -

Well, that's not possible with the default mechanism. One approach to achieve this could be -

  1. Allowing the default transclude in an element in the template.
  2. Programmatically moving child elements(buttons) to appropriate div.
  3. Finally, removing the original transcluded element from DOM in compile or link function of the directive.

Here is a fiddle that demonstrates this approach.


While this approach gives us the intended result, allowing default transclude to happen in an element and then removing it from DOM manually is not really efficient. That's where the transclude argument to compile function and $transclude injectable into Controller come into picture.

Transclude argument to compile function in a directive

The Angular developer guide for directive gives following signature for the compile function of a directive -

function compile(tElement, tAttrs, transclude) { ... }

And here is what it says about the third argument -

transclude - A transclude linking function: function(scope, cloneLinkingFn).

Alright, let's use this function to achieve what we achieved in the last example without the need to transclude into an element and then removing it from DOM.

Here is the fiddle for it.


Please note there is no scope available in compile function and you'll have to pass the element that compile function received as the first argument to the transclude function. You might want to use this approach if you are already using the compile function (which is quite rare, at least for me) and don't need to work with scope. Otherwise, the next approach of injecting $transclude into Controller is the best bet.

Injecting $transclude in a Controller

The Angular developer guide for directive states the following for $transclude injectable for Controller -

$transclude - A transclude linking function pre-bound to the correct transclusion scope: function(cloneLinkingFn).

And here is how we can use it for our buttonBar directive.



Transclude and scope

The Angular developer guide for directive mentions that a directive isolated scope and transclude scope are siblings. Now, what does that mean? If you take a careful look at previous example, you'll notice that parentController creates a scope, the buttonBar directive declares an isolated scope under it and as mentioned in the Angular documentation, transclude creates yet another scope. I have added log statements at appropriate places in the directive to illustrate the relationship between these three scopes. Here is how the output looks like -


You can clearly see the directive isolate scope and transclude scopes are siblings.

Transcluding into an attribute

Well, you can't really do that but achieve something to that effect using the following trick mentioned on stackoverflow.

That covers all I wanted to regarding transclude in AngularJS. Hope this post proves useful to you. Happy ng-ing :).

Update (20th Feb 2013) - scope and transclude argument to compile function

The lack of scope in compile function and the signature of transclude argument to compile function that mentions scope as its first argument leaves a lot of "scope" for confusion :). One thing is certain, there is no scope in compile function. Here it is straight from horse's mouth - https://groups.google.com/d/msg/angular/QbZt4kw5cTY/h-ZIgPO0wrsJ.

So what's the first argument actually? It turns out it's just the jQuery-wrapped root element of the template for the buttonBar directive. This is how it looks like during debugging -

.

Note the absence of scope related behavior such as $$watchers or $$listeners on it.

Here is updated version of third fiddle from top with development version of AngularJS instead of minified version. You can put a debug point as shown in the snapshot above and see for yourself. Hope this clarifies it further.

Update (16th March 2013) - All fiddles updated to latest AngularJS release - 1.1.3

In contrast to what I have mentioned in the earlier update and behavior in earlier releases, the transcludeFn passed to compile function now indeed expects scope as first argument in latest AngularJS release (1.0.5/1.1.3). Therefore the call to this function needs to be made from link function that is returned from compile function as it has access to scope. The fiddle for section Transclude argument to compile function in a directive as been modified accordingly.