{"community_link": "https://github.com/indepth-dev/content/discussions/10"}

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.

Learn advanced Angular features: build the Material tree

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>
app.component.html

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;
}
cdk-tree.component.ts

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>,
cdk-tree.component.html

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>
Select part of the 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>
app.component.html

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.html
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>
app.component.html

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>
cdk-tree.component.html

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>
cdk-tree.component.html

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>
app.component.html

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>;
}
cdk-tree.component.ts

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>
app.component.html

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>
app.component.ts

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>
Providing a context with multiple variables

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>
Define structural directive and iterate over the data source

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.