The nitty-gritty of compile and link functions inside AngularJS directives part 2: transclusion
Transclusion.
A mysterious word I had never heard of before until I met AngularJS. I seriously thought Misko Hevery had invented the word himself, but it appeared to be an existing word:
In computer science, transclusion is the inclusion of a document or part of a document into another document by reference (Wikepedia).
In AngularJS, transclusion is the mechanism that allows you to grab the content of the DOM element of your directive and include it anywhere in the directive's template.
So in the context of AngularJS, we could rephrase the original definition as:
In AngularJS, transclusion is the inclusion of the directive's DOM element content into the directive's template
In this article we investigate the influence of transclusion on the compile
, pre-link
and post-link
functions inside an AngularJS directive.
This article is a follow-up article on "The nitty-gritty of compile and link functions inside AngularJS directives".
The code
Because this article is an extension to part 1, we will use the same code we already discussed previously.
Consider the following HTML markup:
<level-one>
<level-two>
<level-three>
Hello {{name}}
</level-three>
</level-two>
</level-one>
and the following JavaScript:
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
compile: function(tElem, tAttrs){
console.log(name + ': compile');
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link');
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link');
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
providing the following console output:
which can be summarized as:
// COMPILE PHASE
// levelOne: compile function is called
// levelTwo: compile function is called
// levelThree: compile function is called
// PRE-LINK PHASE
// levelOne: pre link function is called
// levelTwo: pre link function is called
// levelThree: pre link function is called
// POST-LINK PHASE (Notice the reverse order)
// levelThree: post link function is called
// levelTwo: post link function is called
// levelOne: post link function is called
and visualized as:
To try it out for yourself, just open this plnkr and take a look at the console.
This is all explained in part 1 so make sure to read it first if you are interested in learning more about how directives without transclusion work.
So let's add transclusion
Transclusion is enabled by adding a transclude
property to the directive definition object:
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
transclude: true,
compile: function(tElem, tAttrs){
console.log(name + ': compile');
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link');
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link');
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
Adding transclude: true
tells AngularJS to capture the content of the directive and make it available in the directive's template. The ng-transclude
attribute can then be used inside the template to specify where you want AngularJS to restore the content.
But before we add the ng-transclude
attribute to template, let's have a look at what we get so far:
To try it out for yourself, just open this plnkr and take a look at the console.
Interesting
First of all notice how the order of the compile
functions is reversed:
// COMPILE PHASE (Notice the reverse order)
// levelThree: compile function is called
// levelTwo: compile function is called
// levelOne: compile function is called
// PRE-LINK PHASE
// levelOne: pre link function is called
// POST-LINK PHASE
// levelOne: post link function is called
and even though we haven't transcluded the actual content yet using ng-transclude
, all compile
functions are called:
So what is happening?
When AngularJS processes the levelOne
directive, it sees that the transclude
property is set to true
in the definition object.
AngularJS now knows it first needs to process the content of the directive's element before it can make the processed content available inside the template.
To accomplish that, AngularJS first needs to process all child elements. So it starts travelling down the element's DOM.
When processing levelOne
's content, it encounters the levelTwo
directive and recursively repeats the same process until it has no more child elements to process.
As soon as the complete child DOM has been processed, AngularJS is ready to start applying the directives' compile
, post-link
and pre-link
functions.
Since we haven't specified an ng-transclude
attribute yet, the processed content is never put back into the DOM and we end up with a black hole where all child elements of levelOne
disappear.
This is rarely useful in real situations, but for the purpose of experimentation it demonstrates how all compile functions are called, even if the content is not effectively transcluded.
Now let's add ng-transclude
To get a complete picture of what's happening, let's add a template
property to our directive definition object with a string value of <div ng-transclude></div>
to tell AngularJS where to put the transcluded content:
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
transclude: true,
template: '<div ng-transclude></div>',
compile: function(tElem, tAttrs){
console.log(name + ': compile');
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link');
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link');
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
and check the output again:
To try it out for yourself, just open this plnkr and take a look at the console.
Let's analyze further
If we summarize the output:
// COMPILE PHASE (Notice the reverse order)
// levelThree: compile function is called
// levelTwo: compile function is called
// levelOne: compile function is called
// PRE-LINK PHASE
// levelOne: pre link function is called
// levelTwo: pre link function is called
// levelThree: pre link function is called
// POST-LINK PHASE (Notice the reverse order)
// levelThree: post link function is called
// levelTwo: post link function is called
// levelOne: post link function is called
we can see that transclusion appears to reverse the order in which the compile
functions are called.
Let's compare the difference visually:
Without transclusion
With transclusion
So why is transclusion reversing the order in which the compile
functions are called?
If we look back at the initial output and how we defined transclusion earlier:
In AngularJS, transclusion is the inclusion of the directive's DOM element content into the directive's template
then we can quickly deduce that AngularJS needs to process the element's DOM content before it can make it available inside the template.
However, the element's child elements can also contain directives that apply transclusion themselves.
So AngularJS has to recursively traverse the DOM first to check if transclusion is enabled in child elements and then compile the DOM backwards to make sure all DOM changes correctly "bubble up" again to the top before the processed DOM is ready to be added to the original directive's template.
The initial black hole that we created before we included the ng-transclude
attribute is a perfect example of this.
Finally, when compilation has finished, the pre-link
and post-link
functions are called in the same way as explained in part 1.
Conclusion
By now you should hopefully have a better understanding of how transclusion affects the compile
, pre-link
and post-link
functions in directives and why it reverses the order in which the compile
functions are called.
If you are still in doubt or have additional questions, please feel free to leave a comment below.
Because of the overwhelming number of questions I have received since the last article, I will also publish follow-up articles on:
- how this relates to the
controller
function of a directive - how this is affected by using a
template
ortemplateUrl
- how this is affected by directive
priority
If you want me to notify you when new follow-up articles are available, please leave your email address below (don't worry, I don't send spam).
Have a great one!
Note
Although it's outside the scope of this article (no pun intended), it is worth mentioning that transclusion also has an important impact on the scope
that is linked to the element (cf. official AngularJS directive page).