This guide was written for Angular 2 version: 2.0.0
When you create more advanced components, the simple input and output mechanisms may not suffice. That's where "transclusion", now also known as content projection, comes into play. In this article we're going to explore the concepts behind transclusion in Angular 1.x and how it translates to Angular 2.
We previously learned about binding input properties to our components in Angular 1.x, which allows us to pass data into a component. In the following example, we pass in a title
and body
property to be rendered by our collapsible-panel
component.
<collapsible-panel title="Click to collapse" body="Hi there!"></collapsible-panel>
While this perfectly works, we can definitely do better. What if we want to pass complete HTML parts into the body of our collapsible panel? Or even entire Angular directives.
In more complex situations it might not be enough to simply use attribute bindings to pass in data, but there's the need for more advanced mechanisms. For that purpose, Angular 1.x has a concept called "transclusion".
A "transcluded component" could be instantiated in our HTML template as follows.
<collapsible-panel title="Click to collapse">
Hi there!
</collapsible-panel>
Rather than passing the body via an attribute binding, we simply define it as a body of our component, just as you're already accustomed with the plain normal HTML elements. Most interestingly, this mechanism allows us to pass in entire HTML parts, dynamic content or even other directives and components.
Name: {{ $ctrl.person.name }}
<collapsible-panel title="Address">
{{ $ctrl.person.name }} lives at the following address.
<address-detail address="$ctrl.person.address"></address-detail>
</collapsible-panel>
ngTransclude
In order to have transclusion work on our component, we have to set the transclude
property to true
and use the ng-transclude
directive in our component's template, which acts as a placeholder for the injected external content.
const collapsiblePanelComponent = {
bindings: {
title: '<'
},
transclude: true,
template: `
<div class="panel">
<div class="panel-heading" ng-click="$ctrl.visible = !$ctrl.visible">
<h3 class="panel-title">{{ $ctrl.title }}</h3>
</div>
<div class="panel-body" ng-if="$ctrl.visible" ng-transclude>
<!-- CONTENT TRANSCLUDED -->
</div>
</div>
`,
controller() {
// collapse by default
this.visible = false;
}
};
angular
.module('app')
.component('collapsiblePanel', collapsiblePanelComponent);
What about transcluding to different destinations? Totally possible, and known as multi-slot or named slot transclusion. Like in our example before, we may want to inject the panel title just like the panel body by making use of transclusion.
<div class="panel">
<div class="panel-heading" ng-click="$ctrl.visible = !$ctrl.visible">
<h3 class="panel-title">
<!-- TRANSCLUDE TITLE CONTENT HERE -->
</h3>
</div>
<div class="panel-body" ng-if="$ctrl.visible" ng-transclude>
<!-- TRANSCLUDE BODY CONTENT HERE -->
</div>
</div>
To enable multi-slot transclusion, we need to change our simple transclude: true
property definition on our component and assign a configuration object to it.
const collapsiblePanelComponent = {
...
transclude: {
titleSlot: 'span'
},
...
};
This tells Angular to look for a span element, and transclude it into our ng-transclude
area with the name titleSlot
. We obviously need to define that transclusion slot in our template accordingly:
<div class="panel">
<div class="panel-heading" ng-click="$ctrl.visible = !$ctrl.visible">
<h3 class="panel-title" ng-transclude="titleSlot"></h3>
</div>
<div class="panel-body" ng-if="$ctrl.visible" ng-transclude></div>
</div>
Note, although we could, we don't need to name the body transclusion slot explicitly. It is our default one. Meaning, everything that is matched by our titleSlot
will go into the title slot, the remaining parts go into the default ng-transclude
area.
Finally, here's how we can use our multi-slot transcluded component and how the full code of our component.
<collapsible-panel>
<span class="title">Click me</span>
Hi there!
</collapsible-panel>
const collapsiblePanelComponent = {
bindings: {
title: '<'
},
transclude: {
titleSlot: 'span'
},
template: `
<div class="panel">
<div class="panel-heading" ng-click="$ctrl.visible = !$ctrl.visible">
<h3 class="panel-title" ng-transclude="titleSlot"></h3>
</div>
<div class="panel-body" ng-if="$ctrl.visible" ng-transclude></div>
</div>
`,
controller() {
// collapse by default
this.visible = false;
}
};
angular
.module('app')
.component('collapsiblePanel', collapsiblePanelComponent);
What if we don't want to provide a title? Well, if we don't provide a required transclusion slot, Angular will throw an exception. Often however, we may want to provide a fallback mechanism by showing a default instead. We can define such an optional transclusion slot by appending a ?
in front of its definition, just as you do with optional component input parameters in Angular 1.x.
const collapsiblePanelComponent = {
...
transclude: {
titleSlot: '?span'
},
...
};
We can then simply define our fallback in the component's template, where normally our transcluded portion would be inserted.
const collapsiblePanelComponent = {
...
template: `
<div class="panel">
<div class="panel-heading" ng-click="$ctrl.visible = !$ctrl.visible">
<h3 class="panel-title" ng-transclude="titleSlot">
Click to expand/collapse
</h3>
</div>
<div class="panel-body" ng-if="$ctrl.visible" ng-transclude></div>
</div>
`,
...
};
Whenever titleSlot
is not defined, we get "Click to expand/collapse" being visualized instead.
Manual transclusion allows to completely control the process of transclusion and adapt it to your needs. Whenever you enable transclusion on your component, you can get a $transclude
function injected. We can then hook into the transclude function by invoking it and passing in a callback function that takes the transcluded element and the according scope.
const collapsiblePanelComponent = {
transclude: true,
...
controller($element, $transclude) {
...
$transclude((transcludedEl, transScope) => {
// find .content DOM element from our template and append
// transcluded elements to it.
$element.find('.content').append(transcludedEl);
});
}
};
We can completely control where to place the transcluded elements into our component template.
Since in the $transclude
function we also get the scope of the transcluded content, we can even manipulate it by attaching additional functions and data which can then be consumed by the transcluded parts.
const collapsiblePanelComponent = {
transclude: true,
...
controller($element, $transclude) {
...
$transclude((transcludedEl, transScope) => {
$element.find('.content').append(transcludedEl);
// attach the controller's toggle() function to the transcluded scope
transScope.internalToggle = this.toggle;
});
}
};
From within the transcluded parts, we can then refer to the internalToggle
function we just added to the transcluded scope before. In this example, a button that is transcluded into our component could thus execute the toggling of the collapsible panel state.
<collapsible-panel>
<p>Hi there!</p>
<button ng-click="internalToggle()">Close</button>
</collapsible-panel>
Just remember to manually destroy the transcluded scope whenever you decide to remove the transcluded content. Otherwise you'll end up with memory leaks.
Don't worry, Angular 2 still has transclusion, sort of. It's now called content projection. Let's briefly explore the main differences.
Angular 1 uses
<ng-transclude>
, Angular 2 uses<ng-content>
ng-content
Content projection in Angular 2 is enabled by default. We don't have to switch on some property on our component such as in Angular 1. All we have to do is to use the <ng-content>
directive to mark the place where our content should be projected. Let's translate our previous example of a collapsible panel into Angular 2.
@Component({
selector: 'collapsible-panel',
template: `
<div class="panel">
<div class="panel-heading" (click)="visible = !visible">
<h3 class="panel-title">
Click to expand/collapse
</h3>
</div>
<div class="panel-body" *ngIf="visible">
<ng-content></ng-content>
</div>
</div>
`
})
class CollapsiblePanelComponent {
visible: false;
}
Just as in Angular 1, we can use the same directive ng-content
and use a select
property on it to selectively choose the elements we want to get projected. The selector must be a valid document.querySelector
expression.
@Component({
selector: 'collapsible-panel',
template: `
<div class="panel">
<div class="panel-heading" (click)="visible = !visible">
<h3 class="panel-title">
<ng-content select="span.title"></ng-content>
</h3>
</div>
<div class="panel-body" *ngIf="visible">
<ng-content></ng-content>
</div>
</div>
`
})
class CollapsiblePanelComponent {
visible: false;
}
In this final code you can see a working example of our Angular 2 implementation of a collapsible panel that uses the content projection mechanism.