I've always had a problem with transclusion. Though I've used it from time to time, using it always felt uncomfortable. I was never really sure what exactly the API was doing, or more importantly, what exactly I was supposed to do when using it.
Based on what I've heard, I'm not alone in this. Transclusion is one of those things people often mention when they talk about their difficulties with Angular.
What is it that makes this a difficult topic then? Conceptually we're not talking about anything hugely complicated - it's essentially just moving some DOM elements around. I think the bigger problems are tangential:
- The API is tricky, with its higher-order functions and "magic arguments".
- Parts of the API are deprecated, and it can be hard to figure out which parts.
- The documentation uses lots of big words ("a transclude linking function pre-bound to the correct transclusion scope").
- There are actually two separate features provided by the same API: Regular transclusion and element transclusion.
It wasn't until writing the transclusion chapter for my book that I felt I had fully grasped this topic. While the book covers the internals of the API, this article is an attempt to write the user guide I wish I'd had before. I want to describe what you can do with transclusion and how you can do it.
The article will not cover the deprecated parts of the API, and will concentrate on what is officially supported.
Even the word "transclusion" seems to be uncomfortable to many people, and is often criticised for being a made-up one.
While it may be a made-up word, it wasn't made up by the Angular team and it actually has an interesting history in computing. As the combination of trans and inclusion I think it actually describes the feature pretty well.
The Basics: Including Content from Another Template
The basic use case for transclusion is straightforward: You want to include some content from one template to another.
For example, say you have a directive called
myCard that renders a piece of content as a visual "card" component: Something that has a title, an image, and some content. Here's what such a directive might look like, without using any transclusion yet:
It's a nice start for such a component, but there's one unfortunate downside to it: We're forced to cram the body content of the card into one big string in the
my-content attribute. This doesn't let us use HTML formatting in the content, and just feels awkward in general.
What we would like to do is what we would naturally do if
<my-card> was any regular HTML tag: Just nest the content as HTML inside it.
This is what transclusion is for. If we enable it by configuring
transclude: true on the
myCard directive, what happens is that Angular grabs the original contents of the
<my-card> element and makes them available for us to attach somewhere in our template.
Concretely, what we get is a fifth argument to the directive's link function. (The four first ones being the element, the scope, the attributes, and the required controllers.)
That fifth argument is a transclusion function, and people usually name it
$transclude in their code.
When we call that function, it returns the transcluded contents, which we can then append into our template:
So this is how transclusion works on the most basic level: The contents of an element are slurped into a function that you can later call so you can attach them somewhere else.
You can think of it as parameterizing your directives with HTML: One of the "input parameters" of
myCard is the body content.
If you prefer not to use link functions and want to use a directive controller instead, transclusion is still fully supported. The transclusion function can be injected to your controller with the name
$transclude. What gets injected is the same exact function that you'd get as the fifth argument to
Transclusion is almost always used with directives that also have templates. This is not a hard requirement though, and you can use transclusion without using a template. See this JSBin for an example with some lower-level DOM manipulation.
Transcluding From a Parent Directive
In our previous example, we're using a jQuery selector to find the element in which to insert the transcluded content. That's a bit clumsy and not idiomatic Angular code. It would be nice to declaratively specify the transclusion point right in the HTML template - to say "please transclude the content here".
This is in fact possible, because when you have a directive with
transclude: true, all directives on child elements will also get access to its transclusion function. So we can introduce a directive like
myContentTransclusionPoint, which uses the transclusion function that it gets from the parent.
No jQuery selectors are needed because an HTML attribute now declares where the transcluded content will go.
The transclusion function you get will be for the "nearest transclusion" found when walking up the DOM tree. It may be from the current element, as in our first example, or some ancestor, as in the second example.
The pattern implemented by
myContentTransclusionPoint is so common, and so generic, that Angular actually ships with a directive that does that for you. Instead of using
myContentTransclusionPoint, you can just use
It's worth noting that this is pretty much all that
ngTransclude does. There's nothing magical about it, which is evident if you look at its source code.
Understanding Transclusion Scopes
Transclusion is nice for static bits of HTML, but what if you use it with dynamic content? That is, content that has its own directives or
Luckily, what happens is pretty much exactly what you'd hope and expect. If we populate the contents of
<my-card> with dynamic data taken from an outer scope, it'll just work:
Though most of the time things will indeed "just work", it is good to understand what's going on here: The content paragraphs are now inside the
myCard template because of transclusion, and
myCard has an isolate scope that knows nothing about the
data.content attribute. How is the content actually available there?
The answer is in how Angular sets up the scope hierarchy for transclusion. When you call the transclusion function, the contents inside the transclusion are linked. They are linked to a special "transclusion scope". It is a scope that inherits from the outer scope outside
However, the transclusion scope is not just an ordinary child of the outer scope. It can't be because otherwise we'd have a problem with getting rid of it: When
myCard eventually gets removed, we want the transclusion scope to get destroyed, because we don't want to leak scopes. Angular scopes get destroyed when their parents do, but if the parent of our transclusion scope is the outer scope, there are no guarantees about how long it'll stay around.
To address this, Angular sets the
$parent of the transclusion scope to a different scope than where the data comes from. The
$parent is based on where the transclusion function is called. In our case, that's the
myCard isolate scope:
This separation between the prototypal parent and the structural parent is what makes transclusion scopes special. As mentioned earlier, however, in most cases things will just work without having to think too deeply about scope hierarchies.
Transcluding Multiple Clones
We've seen how the transclusion function works: You can just call it when you need the transcluded DOM, and you get it as the return value.
There's also an alternative way to call the transclusion function, which is to not expect it to return anything, but to give it a function as a callback argument instead. It will call that function and give it the transcluded DOM:
That callback function you supply is called the clone attach function.
What is the point of doing this though, given that you can also get the content as the return value? In the example above, there is no point, really. But there is a difference in what takes place.
What happens as a side effect when you use a clone attach function is the transcluded content gets cloned before it's linked and given back to you. The elements given to the clone attach function will not be the exact same ones originally sucked into the transclusion. They will be clones of the originals.
This matters especially when you want to transclude the content several times. With cloning you can just do that.
This wouldn't be possible without the clone attach function. If you call the transclusion function several times without one, you get the same DOM elements back every time. If you try to append them to several places, they'll only remain in the last one. Go ahead and try it!
ngTransclude directive also internally uses the clone attach function. This means you can use
ng-transclude several times in your template. Each one will insert a clone of the transcluded content.
Even though it isn't always necessary, most people tend to use the clone attach function whenever they do transclusion. This is also recommended in Angular's own documentation.
The clone attach function isn't strictly just a transclusion feature, since you can also give one to the public link function. If you're doing linking manually and want to link several clones of the same DOM, just pass in the clone attach function as the second argument.
Managing Transclusion Scope Lifecycle
Let's add one more feature to our
myCard directive, which lets the user expand and collapse its contents. When a card is expanded, it calls the transclusion function to create and link a new clone of the content. When the card is collapsed, the transcluded element is removed.
There's a problem with this implementation, which is that it is leaking scopes. Every time we call the transclusion function, a transclusion scope is created for us. But when we empty the transcluded content, Angular has no way of knowing it should now destroy that transclusion scope, so it doesn't.
We need to take responsibility of destroying that transclusion scope. We're able to do that because the transclusion function gives that scope to us as the second argument of the clone attach function. If we just hold on to it, we can call
$destroy() on it later.
$destroy() on the transclusion scope is something you only have to think about if you destroy the transcluded content before your directive itself is destroyed, like what happens in this example. If the transcluded content had the same lifetime as
myCard, the parent-child relationship discussed earlier would take care of things automatically.
The example above a bit contrived, since we could easily accomplish the same kind of behavior just by using
ngIf together with
ngTransclude. There used to be a bug preventing that, but it was fixed in Angular 1.3.
Attaching Custom Data on Transclusion Scopes
There is one more reason you might want to use the clone attach function: It gets called at a specific point in time, which is after the transclusion scope has been created but before the transclusion is linked. This means you can put things on the transclusion scope, and they will be available inside the transcluded content.
For example, the
myCard directive could provide the "collapse" feature as a function on the transclusion scope. If you then wanted to embed a collapse button somewhere inside the transcluded content, you could do that very easily:
Now it starts to make much more sense why the clone attach function is a function and not just a
clone flag or something like that. By the time the transclusion function returns, linking will have been done already, but you want to put things on the transclusion scope before linking!
Instead of having two different API calls for this, Angular provides one API call, with the clone attach function as a callback function that is invoked at just the right time: After the transclusion scope has been created, but before it is linked.
Providing Your Own Transclusion Scope
As discussed earlier, Angular will manage the creation of transclusion scopes for you, so most of the time you don't have to do anything and things will just work.
You can, however, choose not to let Angular create the transclusion scope and provide your own instead. The transclusion function optionally takes a scope object as the first argument (before the clone attach function). If you provide one, it will skip setting up the transclusion scope and just uses the one given.
When would you want to do this? For one thing, it gives you an escape hatch if you run into a corner case with scope management that it doesn't handle. People have done this in the past to get around issues with combining transclusion with
ngRepeat, for example.
There's also an interesting alternative use of transclusion that this makes possible: You can link the transcluded content inside your isolate scope, and not the surrounding scope. So part of the directive's template is still provided through transclusion, but the data used in the transclusion is driven from inside the directive.
For example, you could have a directive like
townView here, that has full control over what data it shows, but allows passing in a dynamic template for how the data is shown:
Do note that using transclusion like this alters the behavior of templates in a way that the directive user may not expect: Expressions inside transcluded content can't access data that they seemingly should. For that reason, I think this pattern should be used sparingly.
Understanding Element Transclusion
Let's now turn our attention to an alternative way you can use transclusion: Doing element transclusion with
The difference between this kind of transclusion and the regular kind is in what becomes the transcluded content: With regular transclusion it is the descendants of the current element that will be transcluded. With element transclusion it is the element itself (including all of its descendants).
On a surface level, this may seem like a minor difference: The transclusion just occurs one level higher than normally. But, if we dig a bit deeper into it, the difference is actually more fundamental. If the element itself becomes part of the transclusion, what exactly is left in the DOM as the container of the transcluded content?
Interestingly, what happens is the element where
transclude: 'element' is used actually disappears. If you look at the DOM, you'll see that it is replaced by an HTML comment. That comment node is also what the directive receives as the
element of its link function.
So what happened to the original element? The answer is that it is now provided by the transclusion function. You can put it back by calling the transclusion function and attaching the result after the comment:
If there are other directives present on the element, their respective priorities define what'll happen to them: Directives with higher priority are linked during the normal linking process, but directives with lower priority are linked when you call the transclusion function.
While this whole exercise is interesting, we haven't really accomplished anything with it. What is element transclusion actually useful for?
Deferred Rendering with Element Transclusion
One use case for element transclusion is when you want to defer the linking and attachment for some part of your UI until something happens.
Since element transclusion essentially removes part of the DOM and makes it available for you to put back using the transclusion function, you have control into when that happens. For example, you might want to only link and attach part of the DOM after some condition on the parent scope becomes true:
What we have here is essentially a simplified version of
ngIf. If you look at the source code of
ngIf, you should now be able to understand what it's doing: It's using element transclusion to conditionally link this part of the DOM only when the condition expression is true.
This also explains why you see HTML comments in your DOM tree whenever
ngIf is used. They're inserted by element transclusion.
Repeated Rendering with Element Transclusion
Another use case for element transclusion is if you want to link and attach part of the DOM several times. For example, you could have a directive that iterates over an array and renders the DOM subtree again for each item in the array, also making each item available on the scope.
This can all be done when we combine the element transclusion mechanism with the clone attach function, because then we can link a new clone for the DOM for each item:
This is essentially a (hugely) simplified version of
ngRepeat. The built-in
ngRepeat directive also uses element transclusion, though studying its source code is not quite as easy as with
ngIf because of the various ways you can use
ngRepeat and all the optimizations it contains. At the heart of it though, it's just an application of element transclusion and the clone attach function.
One might think of an additional use case for element transclusion, which is to use it together with a template, and essentially replace the original element with the template. However, this is not what the feature was designed for and you may run into trouble if you try to use it like that. For these scenarios, using
replace is the way to go, although
replace is deprecated now.
Element transclusion is really designed for use case like the ones we have described here: When you want to defer, skip, repeat, or otherwise control the linking and rendering of some part of the UI. For more information about this, see @caitp's excellent commentary on a related GitHub issue.
It is a bit unfortunate that two features for different purposes (regular transclusion and element transclusion) look so similar in the API. Futhermore, in element transclusion, nothing is really being transcluded anywhere, making it something of a misnomer.
In Angular 2 this is no longer an issue, as the use case for regular transclusion is covered by content tags in components and the use case for element transclusion is covered by the core APIs. (Looking at the current implementation of Angular 2 ng-if, it looks like it can just control when a View is created from a ProtoView.)
Know Your AngularJS Inside Out
Build Your Own AngularJS helps you understand everything there is to understand about AngularJS (1.x). By creating your very own implementation of AngularJS piece by piece, you gain deep insight into what makes this framework tick. Say goodbye to fixing problems by trial and error and hello to reasoning your way through them