Playing With Recursive Ng-Template References In Angular 6.1.10
Yesterday, I started to think about recursive Angular layouts for the first time. And, as someone who has bought into the "everything is a component" point-of-view, my natural instinct was to reach for recursive components. But then, I remembered that Ng-Templates allow for some very dynamic rendering possibilities. As such, before I went the component route, I wanted to see what kind of recursive behavior I could squeeze out of a single component with an embedded template reference.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
The ng-template directive, in Angular, provides two basic behaviors:
- A way to define an Angular template.
- A way to render an existing template reference.
Because of this duality, we can theoretically use ng-template to define an Angular view-partial that uses ng-template to render itself again, recursively (more or less). To experiment with this idea, I created an app component that initializes a tree-like data structure:
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface Tree {
root: TreeNode;
}
interface TreeNode {
label: string;
children: TreeNode[];
}
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
templateUrl: "./app.component.htm"
})
export class AppComponent {
public data: Tree;
public selectedTreeNode: TreeNode | null;
// I initialize the app component.
constructor() {
this.selectedTreeNode = null;
this.data = {
root: {
label: "first",
children: [
{
label: "second-a",
children: [
{
label: "third-first",
children: [
{
label: "ferth",
children: [
{
label: "fiver",
children: []
}
]
}
]
}
]
},
{
label: "second-b",
children: [
{
label: "third",
children: []
}
]
}
]
}
}
}
// ---
// PUBLIC METHODS.
// ---
// I select the given tree node, and log it to the console.
public selectNode( node: TreeNode ) : void {
this.selectedTreeNode = node;
console.group( "Selected Tree Node" );
console.log( "Label:", node.label );
console.log( "Children:", node.children.length );
console.groupEnd();
}
}
As you can see, my Tree is an aggregation of nested TreeNodes. I am also defining a selectNode() method so that we can see how a recursive template can still hook into the public context of the component view.
I came up with two approaches to the recursive ng-template reference. In the first approach, I am using the ngFor directive, which allows you to pass-in a TemplateRef in lieu of in-line markup:
<!-- Define the recursive template. -->
<ng-template #nodeTemplateRef let-node>
<div class="node" [class.node--selected]="( node === selectedTreeNode )">
<a (click)="selectNode( node )" class="node__label">
{{ node.label }}
</a>
<div *ngIf="node.children.length" class="node__children">
<!-- Invoke the recursive template. -->
<ng-template
ngFor
[ngForOf]="node.children"
[ngForTemplate]="nodeTemplateRef">
<!--
NOTE: The "$implicit" property of the ngFor context is what will
be made available to the template ref's implicit let-node binding.
-->
</ng-template>
</div>
</div>
</ng-template>
<!--
Initiate the recursive template rendering. Because our recursive template is going to
be using the ngFor directive to render recursively, the "context" in the recursive
instances is going to be the ngForContext. As such, we have to "mock" the initial
context to look like the context that the ngFor directive will expose internally.
NOTE: If we used ngContainer or ngTemplate to invoke the recursion internally, we'd
have more control over which values were made available at each level.
-->
<ng-template
[ngTemplateOutlet]="nodeTemplateRef"
[ngTemplateOutletContext]="{ $implicit: data.root }">
</ng-template>
<p class="note">
<em>Ng-For Template Rendering</em>
</p>
As you can see, in this approach, I have an initial ng-template directive that assigns the TemplateRef to a view-local variable, "nodeTemplateRef". This template renders a given node and its children. In order to render the children, I am using the ngFor directive; and, I'm passing in the nodeTemplateRef value as the ngFor template (to be used as the collection is unrolled).
I then use another ng-template instance to kick-off the recursive ng-template execution. And, when we run this in the browser and click on a few of the rendered nodes, we get the following output:
As you can see, the ngFor directives was successfully able to recursively render the ngTemplate in which it was defined.
When using the ngFor directive, there's no way to explicitly pass-in a "context" object - the ngFor directive implicitly passes-in the ngForContext object as the template context. This means that my ng-template directive has to use the "implicit" export of the ngFor context as the "let-node" template binding.
In this case, that's not an issue since I only want to pass-in the one value. But, for more flexibility, we can forgo the ngFor directive and simply use another ng-template to render the template recursively (in much the same way that we use an ng-template instance to initiate the recursion):
<!-- Define the recursive template. -->
<ng-template #nodeTemplateRef let-node="node">
<div class="node" [class.node--selected]="( node === selectedTreeNode )">
<a (click)="selectNode( node )" class="node__label">
{{ node.label }}
</a>
<div *ngIf="node.children.length" class="node__children">
<ng-template ngFor let-child [ngForOf]="node.children">
<!-- Invoke the recursive template. -->
<ng-template
[ngTemplateOutlet]="nodeTemplateRef"
[ngTemplateOutletContext]="{ node: child }">
<!--
Because we are using nested ngTemplates (rather than the template
input of the ngFor directive), we have more control over how the
data is made available to the recursive template. In this case,
we're passing "child" through as "node".
-->
</ng-template>
</ng-template>
</div>
</div>
</ng-template>
<!-- Initiate the recursive template rendering. -->
<ng-template
[ngTemplateOutlet]="nodeTemplateRef"
[ngTemplateOutletContext]="{ node: data.root }">
</ng-template>
<p class="note">
<em>(alternative) Ng-Template Template Rendering</em>
</p>
As you can see, this time, instead of relying on ngFor to implicitly export the loop item, I'm using the ng-template's ngTemplateOutletContext property to explicitly define the context for the recursive template. And, when we run this in the browser and click on a few of the rendered nodes, we get the following output:
As you can see, we were able to use the ng-template to:
- Define the recursive template.
- Initiate the recursive template execution.
- Render the template reference recursively.
This approach leads to a bit more mark-up (both in the component view and in the rendered Document Object Model), when compared to the ngFor approach; but, you can see that it provides for a bit more flexibility as you can control the shape of the ng-template context object.
I don't often have to reach for the ng-template directive in order to build an Angular 6 application. But, it is clear that the ng-template directive (and other directives that accept a TemplateRef) afford a great deal of power in view rendering. In this case, we were able to use the ng-template directive to traverse a recursive data-structure within the bounds of a single component.
Want to use code from this post? Check out the license.
Reader Comments
@All,
Today, I wanted to revisit this problem space using recursive components instead of recursive
ng-template
invocation:www.bennadel.com/blog/3513-playing-with-recursive-components-in-angular-6-1-10.htm
As you will see, the mechanics are essentially the same. The difference is we need to start propagating inputs and outputs down and up the resulting DOM. This sounds tedious, but is actually quite straightforward.
Thank you very Much , your Explanation help me lot to understand the Concept .. may God Bless you
@Guest,
Very cool -- glad you found this helpful :D
Hey Ben, Actually I'm a new guy to angular and I found this is very helpful. I want to know how to customize this tree structure by adding collapse and expand toggle icons(like +, -) at each level, it'd be great if you can help with some code explanation with comments same as you did above.
@Munindar,
Yes, that should be totally doable. Though, it would likely be easier with recursive components than with a recursive template since the component would be able to store local state outside of tree-structure. I'll see if I can put together a demo for this.
Note: You could do this with the template approach; but, you would have to store the toggle-state in the tree structure itself.
@Munindar,
Good sir, I tried to explore your question with a new post:
www.bennadel.com/blog/3601-more-fun-with-recursive-components-tree-state-and-one-way-data-flow-in-angular-7-2-13.htm
It's an arbitrarily nested Folder structure in which you can expand / collapse each or all folders. And, to make it a bit more exciting, I'm persisting the expanded view-model the Browser URL so that the Folder Tree will maintain state across page refreshes. I hope this helps.
Recursion is just a fun topic to explore!
How to I access record object inside ng-template elseblock
<ng-template #cellTemplate let-col="column" let-record="record">
@Arul,
Hmmm, I am surprised it isn't available. As long as the Else and Then
ng-template
instances are declared inside of the top-levelng-template
, I would have assumed that thelet-record
should be made available. I'll have to try this out as the behavior is then surprising.@Arul,
So, I just ran a sanity check based on your question:
www.bennadel.com/blog/3610-sanity-check-nested-templates-maintain-lexical-binding-in-angular-7-2-13.htm
As I suggested above, this should "just work". The
ng-template
construct uses lexical binding. As such, your nested templates should automatically have access to therecord
value.What kind of error are you getting?
Ho to get the index for nested arrat loop. i want to display like
1.
1.1
1.2
2
2.1
2.2
2.2.1
2.2.2
3
3.1
Ho we can display index like this.?
Hey thanks for the nice breakdown, I got this working but found it tried to render an empty instance of the repeater, and got undefined for "node". I was able to fix this by putting an *ngIf="node" on the first child of the parent template. Curious if you have any insight as to why this occurs, I thought it might be my data but it happens even if I use your mock.
@Adam,
Oh, that's interesting. Of a repeater is empty, it seems like the
ngFor
wouldn't run (since the collection is empty). It could be a bug in Angular; or in my code. Since you say you can cause the issue with the demo, can you share thethis.data
you were using at the time?@Chethan,
Did you get it figured out? That's a bit tricky for something recursive, especially in this case where it's all a single template, so you lack the benefit of having a Function scope to isolate a given index. That said, you should be able to get the index by changing this:
to:
I think this would give you access to
index
within the template; but, I am not sure how you reference the index of parent iterations.You may be better just trying to use
ul
andli
and have the DOM generate the list-item numbers for you. Though, to be honest, I don't know off-hand how nested numeric list-items work.Ben. It looks like you have recreated something similar to this:
https://material.angular.io/cdk/tree/overview
I know you are getting into Material, so you might find this interesting.
I often use mat-tree to build recursive structures like directory structures etc. And you can plug a data source into a tree, as well!
@Charles,
One of the things I appreciate most about digging into the Material Design library is to see how they use nested components. In my work, I don't often use nested components - usually just one component with inputs. But, there's a lot of flexibility that comes with nested components that I am still trying to wrap my head around and learn from.
Thanks !! great article
awesome you save me a lot of time.
@Sunil, @Ju,
Very cool - glad you are finding this helpful. Though, I will admit that it's a bit tricky at first glance; I would suggest looking into recursive components instead of recursive templates. It's the same principles; but, with a little more intuitive model, I think. If you look up in the comments, I have an example of this.