Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (May. 2008) with: Michael Smith
Ben Nadel at the New York ColdFusion User Group (May. 2008) with: Michael Smith

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.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

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

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.