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.
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 -
- Allowing the default transclude in an element in the template.
- Programmatically moving child elements(buttons) to appropriate div.
- 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.
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.



This is one of the harder concepts to wrap my head around in AngularJS (still is) but your article definitely helped in explaining it a lot better than whatever I've read so far.
ReplyDeleteLook forward to reading more posts related to AngularJS!
Thanks Ali. I'm glad it was of some help to you.
DeleteHi omka, I had your article in my pocket list since a month but just now I've read it and was really...really helpfull...thanks so much...now I've a little doubt about the compile function...
ReplyDeletethe compile function receive 3 arguments, the first is tElement..now..this tElement is the element in your template (div class="span4...)??...not the elements in the dom no?...
my second question is about the linking function...in the documentation shows than the first arg is the scope ..but you pass as first argument the element again and this confuses me a bit...you could explain me a bit more about this piece of code??...
thanks Omkar and have a good day!
I'm glad to hear you found it helpful :).
DeleteRegarding your questions -
1. Yes, that's right. That's the template element.
2. Very keen observation. Actually, Angular documentation is a bit misleading as far as transcludeFn is concerned. The transcludeFn that is passed to compile function (my 3rd fiddle) and $transclude injection happening in the controller (my 4th fiddle) are two different things. The previous has the signature with element as first argument and clone function as second argument while the later has signature with scope as first optional argument and clone function as second argument. This has been explained on the following thread on StackOverFlow - http://stackoverflow.com/questions/13183005/what-exactly-do-you-do-with-the-transclude-function-and-the-clone-linking-functi
thanks so much Omkar...that stackoverflow was very insightful..thanks so much and I hope future articles about angularjs :D ...thanks and have a good day...
DeleteI've been struggling to understand transclude for the past couple of days. You article is the light at the end of the tunnel. Thank you for sharing your knowledge of the subject. Looking forward to read more of your articles.
ReplyDeleteHappy to hear it helped you. My team is developing a huge application based on Angular. Whenever I hit the wall during development, I just dug around a bit and merely sharing my observations on this blog :).
DeleteIn directive, what to use controller for and what to use link function for?
ReplyDeleteIt seems that what I can do in a directive controller, I can do in the link function, and vice versa.
Please give some lighting on this point,
It's more of a separation of concerns. Directive controllers are meant to be used to set initial state of your model i.e. scope and provide behaviour, for example, what happens when a button in your view is clicked. But it's not a place to have DOM manipulation code which is required quite often in the directives. link function is the best place to have it.
DeleteThank you for sharing you have learned, Omkar. This article is a great complementary material to the Angularjs official documentation on the topic of Transclude. It has answered most of my confusion/questions about the topic.
ReplyDeleteThere's one part that I wish you could elaborate on, though. You mentioned there's no scope available in compile function and we should pass the element or the first argument of compile function to the transclude function. But according to the signature of the transclude function, function(scope, cloneLinkingFn), it expects scope as its first argument. This is confusing me a bit.
I'm glad to know it was helpful to you.
DeleteI have added an update section to the bottom of the post with additional information regarding transclude argument to the compile functon. Hope it clarifies your doubts.
Hi, how do I attach directive during 'transclusion'? E.g.
ReplyDeletecontroller: function($attrs, $scope, $element, $transclude) {
return $transclude(function(clone) {
var input = clone.filter(':input');
input.attr('ng-model', 'foo');
return $element.append(clone);
});
}
Will result in --input ng-model='bar'-- but bar is not created in the scope. Anyway great blog, your content should be put in the official AngularJS website!
For dynamically adding directives through another directive, you'll have to inject $compile service into that directive and compile the content. This thread on AngularJS mailing list might be helpful to you - Dynamically adding a directive from within a custom directive
DeleteI'm glad to know you found the blog useful. Regarding putting this content in official AngularJS documentation, I won't mind if they do :).
Omkar, your example code for "Injecting $transclude in a Controller" breaks with the latest AngularJS (1.0.5/1.1.3). I switched back to 1.1.0 and the code just ran fine.
ReplyDeleteI stepped through the code and found that the content of the "clone" argument differs in the latest AngularJS. More specifically, clone.scope gave me "undefined".
Please take a look and perhaps add an update to your helpful article. Thanks.
Thanks for letting me know and the helpful debugging pointer. I have updated all fiddles and tweaked them to work with latest AngularJS(1.1.3) release.
DeleteThis article I was waiting for. My initial idea was to take a look in the angular source on the ng-repeat. But this docs seems to make my day! The official docs realy sucs...
ReplyDeleteSimply ... thanks Omkar
ReplyDeleteCheers mate :)
Deletevery helpfull, thanks
ReplyDeleteI'm glad it was useful to you.
DeleteThanks for this! very Helpful! You should add this post to the Angular docs...
ReplyDeleteI'm glad you found it helpful :)
Delete