Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Hemant Khandelwal
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Hemant Khandelwal@khandelwalh )

Playing With Recursive Ng-Template References In Angular 6.1.10

By Ben Nadel on

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:


 
 
 

 
 We can use ng-template and ng-for to recrusively render a view partial in Angular 6.1.10. 
 
 
 

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:


 
 
 

 
 We can use ng-template to recrusively render an ng-template view partial in Angular 6.1.10. 
 
 
 

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.



Reader Comments

@All,

Today, I wanted to revisit this problem space using recursive components instead of recursive ng-template invocation:

https://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.

Reply to this Comment

Thank you very Much , your Explanation help me lot to understand the Concept .. may God Bless you

Reply to this Comment

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.

Reply to this Comment

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

Reply to this Comment

@Munindar,

Good sir, I tried to explore your question with a new post:

https://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!

Reply to this Comment

How to I access record object inside ng-template elseblock
<ng-template #cellTemplate let-col="column" let-record="record">

        <input
          id="eldmLobDomainAlias_{{ record.id }}"
          name="domainAlias"
          #domainId
          type="search"
          [ngModel]  *ngIf="record.alias === record.name; then thenBlock ; else elseBlock"
          (blur)="this.changeAction(record)"
          maxlength="25"
          (search)="this.utilsComponent.addOrEditAlias(record, domainId.value)"
          class="domainAliasInput tk-input-masking ng-pristine ng-valid ng-touched form-control"

        />
        <ng-template #elseBlock>"diff"</ng-template>
        <ng-template #thenBlock>"Same"</ng-template>
      </ng-template>
Reply to this Comment

@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-level ng-template, I would have assumed that the let-record should be made available. I'll have to try this out as the behavior is then surprising.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.