Learn advanced Angular features: build the Material tree
Learn about ng-content, ng-template, ContentChild and structural directives to build Angular material "mat-tree". These are badly documented but powerful and advanced concepts available in Angular.

Do you already use a tree component in your app? Before implementing it for a company project, I looked at the one proposed by the Angular CDK.

<cdk-tree [dataSource]="dataSource">
<cdk-tree-node *cdkTreeNodeDef="let node">
<span>{{ node.label }}</span>
</cdk-tree-node>
</cdk-tree>
I work with Angular daily but I didn't know how to implement this. Is it a syntax reserved for the Angular team? Well, no, but it requires the use of some advanced features of the framework. The trick is most of these features are only briefly described in Angular documentation.
In this article, I'll explain each feature you need to build a tree component with the very same API of Angular Material.
- Content projection (ng-content)
- Dynamic templates (ng-template, ngTemplateOutlet)
- Query content (@ContentChild)
- Structural directives
This isn't a complete guide for these badly documented features but a concrete usage example with some explanations. I'll do my best to include links to the best articles I found explaining each feature in-depth!
Content Projection
The most interesting part in the cdk-tree
component is it accepts a dynamic html template between its start and end tags. Let’s try to pick this template and use it to display each node from dataSource
input.
<cdk-tree [dataSource]=”dataSource”>
<span>My node template</span>
</cdk-tree>
Nothing special here. This is some regular HTML code. HTML pages are built from element compositions, right? The difference between regular HTML elements and our example is that cdk-tree
is a component. This piece of code between start and end tags is the HTML element content.
Let's create the cdk-tree
component and see how its content is handled.

@Component({
selector: 'cdk-tree',
template: `
<h1>cdk-tree</h1>
<div class="border--black">
<!-- Expect the content to appear here -->
</div>
`,
})
export class CdkTreeComponent {
@Input() dataSource;
}
This is disappointing, the span
in its content isn't rendered. It's like there is no content. By default, a component only renders the HTML inside their own template. You need ng-content
to output the content. This is similar to slots for Web components and Vue.js but also, React children prop.

<h1>cdk-tree</h1>
<div class="border--black">
<!-- Replaced by cdk-tree content -->
<ng-content></ng-content>
</div>,
It renders the content now! You achieved content projection. The parent component creates the content elements but they’re rendered in another component. It looks like the piece of DOM moved from one place to another but it didn't. The CdkTree component only reference the content which belongs to the parent component.
Projected content is created by the parent component but rendered in the child component.
This special element accepts a select attribute which behaves like a CSS selector. You can choose to only render a few parts of the content. In the following example, we select the node header from the content to wrap it in a header element.

<!-- app.component.html -->
<cdk-tree [dataSource]="dataSource">
<span header>Node title</span>
<span>My node template</span>
</cdk-tree>
<!-- cdk-tree.component.html -->
<h1>
<!-- Extract the HTML element with header attribute from content -->
<ng-content select="[header]"></ng-content>
</h1>
<!-- Get everything not yet selected from the content -->
<ng-content></ng-content>
For the cdk-tree
, each node renders with different values. it's necessary to have a dynamic template as content. The output HTML must be different for each node according to their label
property.
<cdk-tree [dataSource]="dataSource">
<!-- Error: Property 'node' does not exist on type 'AppComponent' -->
<div>{{ node.label }}</div>
</cdk-tree>
It's not working with ng-content
. It only works if you define a node
property in the App component using the cdk-tree
. Yet, it must be the cdk-tree
responsibility to know about the nodes with dataSource
and loop over.
This solution with ng-content
can't meet our requirements but it’s a good first step. In other situations, this content projection method may be enough to do the job.
For more information about ng-content
, check out this issue asking for more documentation. It references this Medium article as an example. If you're curious as to why ng-content
isn't only about moving DOM, learn about views.
Dynamic template
With ng-content
, we could render a template with dynamic data bound to a parent (host) component into the cdk-tree
. Now it's time to let the cdk-tree
provide data to the template for each node.
<cdk-tree [dataSource]="dataSource">
<!-- Can’t get a reference to the node to display -->
<div>{{ node.label }}</div>
</cdk-tree>
App component renders the template in its own context, not in the cdk-tree context. Yet, the content appears in the cdk-tree thanks to content projection.
Instead of HTML content, we need to provide a template. It's like a blueprint: it defines the HTML content but also accepts data to make it dynamic. Later, the template will be used to generate DOM elements.
<cdk-tree [dataSource]="dataSource">
<!-- Creates a template with node parameter -->
<ng-template let-node="data">
<div>{{ node.label }}</div>
</ng-template>
</cdk-tree>
There it is, some new fancy syntax. ng-template
is the tag to define a template. Remember this is a blueprint, you can try to render it with ng-content
but it'll render nothing. It needs to be instanced to render DOM elements, like the HTML5 template tag used for Web Components.
This template takes a let-node
attribute. The syntax looks like the JavaScript variable definition. It's because you declare a variable called node
with data
as value. The name for the node
variable can be anything, it only has to match the name you want to use in the template. The data
property will be injected during instantiation and in our case will be provided by CdkTree component.
When you instantiate a template you can provide a context to define variables and make it dynamic.
<ng-container
[ngTemplateOutlet]="referenceToTheTemplate"
[ngTemplateOutletContext]="{ data: { label: 'My node' } }">
</ng-container>
This is the NgTemplateOutlet directive job. If you compare to the documentation, this is a more verbose syntax. It's on purpose, this syntax looks easier to understand to me but it works the same with the shorter syntax. The ngTemplateOutlet
property contains the template, while ngTemplateOutletContext
property contains the context (including data
property).
Where does referenceToTheTemplate
come from? It's a template reference variable. Using these template variables, you can flag any piece of HTML and get a reference to it. It's like the combination of id attribute and getElementById
but using a hashtag syntax.

<ng-template #referenceToTheTemplate let-node="data">
<div>{{ node.label }}</div>
<ng-template>
<ng-container
[ngTemplateOutlet]="referenceToTheTemplate"
[ngTemplateOutletContext]="{ data: { label: 'My node' } }">
</ng-container>
The result looks a lot like the one we had with ng-content. The node
value isn’t provided by the host component anymore. It's a cdk-tree
job which knows about the dataSource
. In the next section, you’ll learn how to move the node template back to the host component.
For more information about ng-template
and ngTemplateOutlet
, I suggest this great article. There is also a good talk on Angular Connect based on a real use case. If you didn't know ng-container
, the author talks about it in the article just mentioned (the same thing as React Fragments). This feature of dynamic templates is also available on Vue.js as scoped slots.
Content query
We succeed to instantiate a template using ngTemplateOutlet
with a reference variable. Yet, it only works when the same component defines both the template and the reference variable.
<cdk-tree [dataSource]="dataSource">
<ng-template let-node="data">
<div>{{ node.label }}</div>
<ng-template>
</cdk-tree>
In the cdk-tree
use case, the template is defined in a host component but instanced within the cdk-tree
. We need to get the node template from the component content.
ng-content
can access the component content but don't provide the template reference. You can query the component content using the ContentChild decorator.
@Component({
selector: 'cdk-tree',
template: `
<!-- Node template is back to the host component -->
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ data: { label: 'My node' } }"
></ng-container>
`,
})
export class CdkTreeComponent {
// Get node template reference from component content
@ContentChild(TemplateRef) nodeTemplate: TemplateRef<any>;
}
ContentChild
can query the component content using a selector. The special value TemplateRef
given to ContentChild
selects the first ng-template
from the content. This decorator actually acts like document.querySelector.
Angular also provides other similar decorators. ContentChildren returns a QueryList
, the equivalent of document.querySelectorAll. ViewChild and ViewChildren can query elements inside the component template.
Query decorators are more powerful than our small use case. They enable access to actual DOM elements but also Component instances.
@ContentChild(CdkTreeComponent) treeInstance: CdkTreeComponent;
@ContentChild('referenceToTheTemplate', { read: TemplateRef })
nodeTemplate: TemplateRef<any>;
The first example with a class selector returns the component instance by default. The second example is the verbose form for selecting a template. It uses a template reference variable for selector plus the read parameter. The query can read the following values: TemplateRef
, ElementRef
, ViewRef
, ViewContainerRef
and instances of components and directive using the class defining them.
You’re getting closer to the final implementation. The cdk-tree
provides the current node with ngTemplateOutletContext
. Besides, the node template comes from the host component thanks to ContentChild
. The last step is to match Angular Material tree syntax.
Check ViewChild documentation for more selectors examples (it's the only one giving information about it). If you're interested in the values the query can return, read this In-depth article. For a comparison between ViewChildren
and ContentChildren
, check this blog post.
Structural directives
Let's make ng-template
, ngTemplateOutlet
and @ContentChild
work together. That's all we need to build a decent cdk-tree
. Yet, there is room for improvement. ng-template
syntax to declare variables from context properties is hard to grasp.
<cdk-tree [dataSource]="dataSource">
<ng-template let-node="data">
<div>{{ node.label }}</div>
</ng-template>
</cdk-tree>
I'm sure you know about *ngIf
and *ngFor
directives. The second does very similar operations compared to our cdk-tree
. It takes the template to display one element and loops over the input list. Then it renders the template and provides the current list item in the context.
<ul>
<li *ngFor="let node of nodes; let index = index">
{{ node.label }}
</li>
</ul>
The little star is syntactic sugar for declaring an ng-template
and its input variables. This syntax helps to build structural directives which accept a TemplateRef
as a parameter and possibly render it with a context.
Don't be fooled by simpler attribute directives which mount on rendered DOM.
<div *cdkTreeNodeDef="let node; let isLeaf = leaf">
{{ isLeaf ? '' : '>' }} {{ node.label }}
</div>
<!-- Without structural directive -->
<ng-template let-node let-isLeaf="leaf">
<div>{{ isLeaf ? '' : '>' }} {{ node.label }}</div>
</ng-template>
Here is a basic example showing how Angular changes nodes with the (*) asterisk syntax into ng-template
. The string which defines the template variables must respect Angular micro syntax. Did you notice the node
variable? It's not bound to a variable, it matches the default property $implicit
from the template context.
// cdk-tree.component.ts
const context = {
$implicit: { label: 'My node' },
leaf: true
};
// cdk-tree.component.html
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="context”
></ng-container>
cdkTreeNodeDef
structural directive matches the name in Angular Material cdk-tree
. To create a structural directive, define a class with the Directive decorator. There is no need for more code as we only use it to benefit from the (*) asterisk syntactic syntax.

// cdk-internal-tree-node-def.directive.ts
@Directive({
selector: '[cdkTreeNodeDef]'
})
export class CdkTreeNodeDefDirective {}
// cdk-tree.component.html
<ng-container *ngFor=”let data of dataSource”>
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ $implicit: data }">
</ng-container>
</ng-container>
If you write this component in a library, don't forget to also export the directive. So you're able to query it using @ContentChild
and make it available for users.
For more information about structural directives and micro syntax check out this article. It’s worth mentioning the official documentation which is quite complete but hard to grasp.
Wrapping up
You should now have learned each advanced Angular feature you need to know to build your cdk-tree
. Angular Material tree implementation differs a bit, as it's using createEmbeddedView
instead of ngTemplateOutlet
. The rest of the code is very similar.
Check out this Stackblitz for the whole code and demonstration
Don't forget the features we used today are quite advanced. For sure, you don't create structural directives everyday. Most of the time, simpler is better. After the 2020 Developer survey, Angular team realised developers are waiting for better documentation. Content projection is shortlisted on topics to address during Q1 2021.
If you liked this article or if you are curious about how we innovate at Smart AdServer, take a look at our official Smart AdServer blog. See you there!
Thanks to the reviewers who helped me to make this article better. Thomas Mainguy, Yann Mentzos, Erwan Azzoug, Romain Pertin and Amy Bornong from Smart AdServer. Also Hayden Braxton, Max Koretskyi, Natan Br and Amadou Sall from InDepthDev community.