Have you ever worked with a tree component in your application? When I needed one for a project at my company, the first thing I did was explore the solution offered by Angular CDK.

Learn advanced Angular features: build the Material tree — figure 1

<cdk-tree [dataSource]="dataSource">
  <cdk-tree-node *cdkTreeNodeDef="let node">
    <span>{{ node.label }}</span>
  </cdk-tree-node>
</cdk-tree>

Even though I use Angular on a daily basis, I had no idea how to get this working. Is it some kind of special syntax reserved for the Angular core team? It turns out it’s not—but it does depend on several advanced framework capabilities. The real issue is that most of these capabilities are only touched on briefly in the official Angular documentation.

Down below, I’m going to walk through every feature that’s needed to create a tree component using the exact same API as Angular Material. Here’s the lineup:

  • Content projection (ng-content)
  • Dynamic templates (ng-template, ngTemplateOutlet)
  • Content queries (@ContentChild)
  • Structural directives

This isn’t meant to be an exhaustive tutorial on these underdocumented concepts, but rather a hands-on example with clear explanations. I’ll also point you to the best resources I’ve found for diving deeper into each topic.

Content Projection

The coolest thing about the cdk-tree component is that it lets you pass a dynamic HTML template between its opening and closing tags. Let’s figure out how to grab that template and apply it to render each node from the dataSource input.

<cdk-tree [dataSource]=”dataSource”>
  <span>My node template</span>
</cdk-tree>

app.component.html

There’s nothing out of the ordinary here—just plain HTML. After all, pages are made by combining elements, right? What sets this apart from a standard element is that cdk-tree happens to be a component. The markup enclosed between its opening and closing tags is what’s known as the HTML element content.

Now, let’s build the cdk-tree component and examine how that content gets processed.

Learn advanced Angular features: build the Material tree — figure 2

@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;
}

cdk-tree.component.ts

Unfortunately, the span placed inside its body fails to appear—it's as if no content exists. By default, a component renders solely the HTML from its own template. To display that content, you must use ng-content, which functions like slots in Web components and Vue.js, as well as the children prop in React.

Learn advanced Angular features: build the Material tree — figure 3

<h1>cdk-tree</h1>
<div class="border--black">
   <!-- Replaced by cdk-tree content -->
   <ng-content></ng-content> 
</div>,

cdk-tree.component.html

Now the content is displayed! You've successfully implemented content projection. The parent component generates the content elements, yet they are rendered within a different component. Although it appears the DOM fragment was relocated, it wasn’t. The CdkTree component merely references content belonging to the parent component.

Parent-created projected content is displayed inside the child component.

This unique element contains a select attribute that functions similarly to a CSS selector. You have the option to render only specific portions of the content. In the upcoming example, we pick the node header from the content and enclose it within a header element.

Learn advanced Angular features: build the Material tree — figure 4

<!-- 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>

Pick a portion of the content

In the cdk-tree, every node displays distinct values, so you need a template that adapts at runtime. The resulting HTML has to vary per node depending on its label property.

<cdk-tree [dataSource]="dataSource">
   <!-- Error: Property 'node' does not exist on type 'AppComponent' --> 
   <div>{{ node.label }}</div>
</cdk-tree>

app.component.html

ng-content fails here. A node property must be defined inside the App component with cdk-tree for this to function. Still, the cdk-tree itself should handle node discovery through dataSource and iterate accordingly.

This ng-content approach doesn’t satisfy our needs, though it serves as a decent starting point. In alternative contexts, such content projection might be sufficient on its own.

To dive deeper into ng-content, see this issue requesting additional docs, which points to this Medium write-up as a reference. If you're wondering why ng-content involves more than simply relocating DOM, read up on views.

Dynamic template

Initially, ng-content let us place a template with data bound to a parent (host) component into the cdk-tree. Our next move is to have the cdk-tree supply each node its own data for that template.

<cdk-tree [dataSource]="dataSource">
  <!-- Can’t get a reference to the node to display --> 
  <div>{{ node.label }}</div>
</cdk-tree>

app.component.html

App component renders its template within its own context, rather than inside the cdk-tree's context. Even so, the content shows up in the cdk-tree because of content projection.

We supply a template rather than HTML content. Think of it as a pattern: it lays out the HTML structure, but it also takes data to become dynamic. Down the road, that template serves to create 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>

app.component.html

Here you go — fresh new syntax. The tag ng-template is how a template gets defined. Keep in mind, it’s just a blueprint: even if you attempt to render it via ng-content, nothing will show. It must be instantiated first to produce DOM elements, just like the HTML5 template tag that backs Web Components.

Notice that this template carries a let-node attribute. That syntax mirrors how variables are declared in JavaScript, because you’re really defining a variable named node whose value is data. You’re free to pick any name for that variable—it just needs to align with what you reference inside the template’s body. At instantiation time, the data value gets injected, and in our setup, the CdkTree component is what supplies it.

Whenever you instantiate a template, you can pass a context that sets up variables, making the whole thing flexible and reactive.

<ng-container 
  [ngTemplateOutlet]="referenceToTheTemplate"
  [ngTemplateOutletContext]="{ data: { label: 'My node' } }">
</ng-container>

Presenter: cdk-tree.component.html

This is where the NgTemplateOutlet directive steps in. Compared with the official docs, this form is more elaborate. That’s deliberate—I find this syntax more readable, though it behaves identically to the more concise variant. The ngTemplateOutlet input holds the template itself, whereas the ngTemplateOutletContext input supplies the context, which includes the data field.

Dive deeper into ngTemplateOutlet

What is the source of referenceToTheTemplate? It’s a template reference variable. With such variables, you can tag any segment of markup and obtain a handle to it. Think of it as pairing the id attribute with getElementById, but implemented through a hashtag syntax.

Learn advanced Angular features: build the Material tree — figure 5

<ng-template #referenceToTheTemplate let-node="data">
  <div>{{ node.label }}</div>
<ng-template>

<ng-container
  [ngTemplateOutlet]="referenceToTheTemplate"
  [ngTemplateOutletContext]="{ data: { label: 'My node' } }">
</ng-container>

cdk-tree.component.html

The output closely matches what we achieved earlier with ng-content. The host component no longer supplies the node value. Instead, the cdk-tree, which has access to the dataSource, is responsible for it. Up next, you’ll discover how to relocate the node template back to the host component.

For additional reading on ng-template and ngTemplateOutlet, I recommend this excellent resource. There’s also a compelling presentation on Angular Connect featuring a real-world application. If ng-container is unfamiliar, that same article covers it (equivalent to React Fragments). Additionally, the concept of dynamic templates exists in Vue.js under the name scoped slots.

Content query

We managed to render a template through ngTemplateOutlet using a reference variable. The catch, however, is that this approach requires both the template and the reference variable to reside in the same component.

<cdk-tree [dataSource]="dataSource">
  <ng-template let-node="data">
    <div>{{ node.label }}</div>
  <ng-template>
</cdk-tree>

app.component.html

When working with cdk-tree, the view template lives inside a parent component, but it gets rendered within the cdk-tree itself. The challenge is retrieving that node template from the parent component’s content.

With ng-content, you can reach the component’s content, but you still lack a direct reference to the template. To obtain it, you can extract the content by leveraging the ContentChild decorator for querying.

@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>;
}

cdk-tree.component.ts

Using ContentChild, you can extract a particular piece of content from the component by providing a selector. If the special token TemplateRef is passed to ContentChild, it grabs the initial ng-template found inside the projected content. In essence, this decorator behaves like document.querySelector.

There are additional decorators of the same kind available in Angular. When you need the counterpart of document.querySelectorAll, ContentChildren gives you a QueryList. Meanwhile, ViewChild and ViewChildren target elements located within the component's own template.

Our example only scratches the surface of what query decorators can do. Beyond reaching into DOM nodes, they also provide direct references to Component instances.

@ContentChild(CdkTreeComponent) treeInstance: CdkTreeComponent;

@ContentChild('referenceToTheTemplate', { read: TemplateRef })
nodeTemplate: TemplateRef<any>;

The initial example using a class selector defaults to returning the component instance. The next example, which is the extended form, targets a template through a template reference variable combined with the read parameter. The query supports retrieving TemplateRef, ElementRef, ViewRef, ViewContainerRef, and component or directive instances via their defining classes.

You’re nearing the completion stage. The cdk-tree supplies the current node through ngTemplateOutletContext. Meanwhile, the node template originates from the host component via ContentChild. The only remaining task is aligning with Angular Material tree syntax.

Consult the ViewChild documentation for additional selector examples—it’s the sole resource covering this topic. If the query’s potential return values interest you, review this In-depth article. For a contrast between ViewChildren and ContentChildren, see this blog post.

Structural directives

Now, it’s time to combine ng-template, ngTemplateOutlet, and @ContentChild seamlessly. These three elements suffice to construct a functional cdk-tree. Still, there’s potential for refinement. The ng-template syntax for declaring variables derived from context properties remains challenging to master.

<cdk-tree [dataSource]="dataSource">
  <ng-template let-node="data">
     <div>{{ node.label }}</div>
  </ng-template>
</cdk-tree>

app.component.html

You’re likely already familiar with the *ngIf and *ngFor directives. The latter performs tasks closely resembling what our cdk-tree does—it grabs the template for rendering a single item, iterates through the supplied array, and for each iteration it displays the template with the current entry placed into the context.

<ul>
  <li *ngFor="let node of nodes; let index = index">
    {{ node.label }}
  </li>
</ul>

The asterisk notation is essentially shorthand for an ng-template along with its input variables. This concise form is what enables structural directives — those that take a TemplateRef argument and optionally render it against a given context.

Be careful not to mistake this for the more basic attribute directives, which attach themselves to already existing DOM elements.

<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>

app.component.ts

Consider a minimal illustration of how Angular transforms nodes using the (*) asterisk notation into ng-template. The string that declares template variables has to follow Angular micro syntax. Have you noticed the node variable? It plays no direct role in binding; instead, it corresponds to the default $implicit property in 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>

Multi-variable context provisioning

In Angular Material’s cdk-tree, the cdkTreeNodeDef structural directive serves as a name match. A structural directive is built by declaring a class adorned with the Directive decorator. Beyond that, no additional logic is required, since the sole purpose is to leverage the asterisk (*) syntactic sugar.

Learn advanced Angular features: build the Material tree — figure 6

// 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>

Create a structural directive and loop over the data source

When building this component as part of a library, make sure the directive is exported as well. This allows you to access it with @ContentChild and expose it to consumers.

For a deeper look at structural directives and their micro syntax, refer to this piece by Netanel. The official docs are also worth checking out — they’re detailed, though not the easiest to follow.

Wrapping up

By now, you’ve covered every advanced Angular feature required to assemble your cdk-tree. The Material tree implementation takes a slightly different path, relying on createEmbeddedView rather than ngTemplateOutlet. Beyond that, the logic stays largely the same.

The full source and a live demo are available on this Stackblitz

Keep in mind these features are rather sophisticated. Structural directives aren’t something you’ll likely write on a daily basis. In many cases, simplicity wins. Insights from the 2020 Developer survey prompted the Angular team to prioritize clearer documentation, and content projection is one of the topics they plan to address in the first quarter of 2021.

Enjoyed the read? Curious about how we push innovation at Smart AdServer? Head over to our official blog. See you over there!


Many thanks to the reviewers who contributed to improving this article: Thomas Mainguy, Yann Mentzos, Erwan Azzoug, Romain Pertin and Amy Bornong from Smart AdServer, as well as Hayden Braxton, Max Koretskyi, Natan Br and Amadou Sall from the InDepthDev community.