Build tree with NgTemplate & NgTemplateOutlet (Angular)

When you are thinking about view element manipulation and want to use the infrastructure of angular in the right way, you are in the right story.

Many developers are using different components to show the same information.

for example, working with cross-platform and supporting different sizes of display. developers got the same data from the backend and just need to manipulate it and show that with a different view.

ng-template:

First, need to understand that if you are not calling ng-template and just write it in your HTML you won’t see it:

<div> Hello </div>
<ng-template> World </ng-template>

Because no one using this template the output will be:

Hello

Let’s take a look on this example:

<div> Hello </div>
<ng-template> Global </ng-template>
<ng-template> World </ng-template>

Again we got “Hello” the same as before, let’s go over and see where Angular using this ability and we can use it to our tree.

Using ng-template via structural directive *ngFor :

<div *ngFor="let item of ["Hello","Global","World"]"> 
{{ item }}
</div>

But under the hood we can find this implementation of Angular:

<ng-template ngFor let-item [ngForOf]="["Hello","Global","World"]">
{{ item }}
</ng-template>

From the last example, developers can understand how Angular use ngFor internally, with the option to get ngTemplateRef to directive via the constructor, another option is to add your template according to the dynamic data of ngFor: ([“Hello”, ”Global”, ”World”])

@Input() 
set ngForTemplate(value: TemplateRef<NgForOfContext<T, U>>) {
this._template = value;
}

I can say that’s a lot of developers are using ng-template like this example

<div *ngFor="let item of ["Hello","Global","World"]" ; let odd=odd> 
<div *ngIf="odd">{{ item }}</div>
<div *ngIf="!odd">{{ item }}</div>
</div>

OR another option:

<div *ngFor="let item of ["Hello","Global","World"]" ; let odd=odd > 
<div *ngIf="odd; then oddTemplate else evenTemplate">{{ item }}</div>
<ng-template #oddTemplate> {{item}} - odd </ng-template>
<ng-template #evenTemplate> {{item}} - even </ng-template>

Now, we understand angular using ng-template on structural directive “ngFor” we can learn if the index is Odd/Even, it’s good starting to do it easier.

We can overwrite the Template with a setter that exists under ngFor source, so we can do manipulation dynamically according to the data

<div *ngFor="let item of ["Hello","Global","World"]" ; let odd=odd ; template = odd ? oddTemplate : evenTemplate> 
</div>
<ng-template #oddTemplate> {{item}} - odd </ng-template>
<ng-template #evenTemplate> {{item}} - even </ng-template>

It’s nice we got a cleaner&smarter code of view manipulation.

How does it’s work ???

Under ngFor Angular inject templateRef and because they change it to ng-template in HTML, we will get this unique template as inject internal to the constructor. it means the content we have intenal

<div *ngFor ….> content </div>

will get a layer of ng-template and from TS file of ngFor implementation angular using reference to this template. with template & viewContainerRef we can use createEmbededView of the content and to push the context (data) eqch content we are going to add to the DOM.
if we want to overwrite it so Angular extend the option to set ngForTemplate(…) and we did manipulatation on the look and feel with different context of data.

we got flexible logic under internal ngFor/ngForOf without changing the code. just use it outside.

We talked about very small logic, that can be more complicated but again we want to find something cleaner and smarter.

Let’s think about building a Tree (but now without using any third party as Angular Material, …), let’s build it alone.

Before few words on ngTemplateOutlet :

With TemplateOutlet ability, developers can create EmbeddedView very fast and simpler and inject the relevant context (data).

Let’s change a little bit last code and make it works:

<div *ngFor="let item of ["Hello","Global","World"]; let odd=odd">
<ng-container *ngTemplateOutlet="odd ? oddTemplate : evenTemplate);context:{ item }">
</ng-container>
</div>
<ng-template #oddTemplate> {{item}} - odd </ng-template>
<ng-template #evenTemplate> {{item}} - even </ng-template>

How it’s happened in ngFor from TS file:

const view = this._viewContainer.createEmbeddedView(                this._template, new NgForOfContext<T, U>(null!, this._ngForOf!, -1, -1),                currentIndex === null ? undefined : currentIndex);

Angular using ViewContainerRef internal ngFor (reference to the next appened in DOM under the same container), to createEmbededView with the relevant context that get from ngFor according to the current context, in our example from [“Hello”,”Global”,”World”], create 3 embeddedView with diffrent context under the same container. this logic happened with ngTemplateOutlet when developers inject internal to the directive of ngTemplateOutlet template & context

So now we are ready for the last example of Tree, let’s plan it.

  1. The tree’s a structure with parent and children can be with the same interfaces for both Parent and Children

inode.interface.ts:

export interface INode {
children: INode[] ;
view: IViewNode;
}
export interface IViewNode {
selected: boolean;
expanded: boolean;
isVisible: boolean;
}

HTML:

/// container to inject the relevant template if node.nodes.length:
<ng-container *ngTemplateOutlet="data?.children?.length ? GroupTemplate : LeafTemplate);context: {item: data}">
</ng-container>

// Template for Leaf
<ng-template #LeafTemplate let-item="item">
<div>
({{item?.children?.length ? 'Group' : 'Leaf'}})
<span>{{ item?.view?.name }}</span>
<span (click)="onExpandClicked({type: 'expand', data: item })">+
</span>
</div>
</ng-template>
// Template for Group:<ng-template #GroupTemplate let-item="item">
<ng-container *ngTemplateOutlet="LeafTemplate;context:{item: item}">
</ng-container>
<div style="padding-left: 10px;" *ngFor="let child of item?.children">
<ng-container *ngTemplateOutlet="child?.children?.length ? GroupTemplate : LeafTemplate);context:{item: child}">
</ng-container>
</div>
</ng-template>

That's it we are ready to run it, just need data to move and we are done:

data: INode[] = {  view: { selected: false, expanded: false, isVisible: false},
children: [{
view: {
selected: false,
expanded: false,
isVisible: false
},
children: [{
view: { selected: false, expanded: false, isVisible: false},
children: [{
view: {
selected: false,
expanded: false,
isVisible: false
},
children: [{
view: {
selected: false,
expanded: false,
isVisible: false
},
children: []}]
}]
}]
},
{
view: { selected: false, expanded: false, isVisible: false},
children: []
}]
};

Result:

+Group
+Group
+Group
-Leaf

Conclusion:

the best practice is to open sources of Angular documents and to get a lot of information about how to work on the UI side and what is the abilities and extend them when needed.

To review the source code of ngFor, ngIf

Full-Stack Web Developer