Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Amsterdam) with: Terrence Ryan and David Huselid and Alison Huselid and Claude Englebert and Ray Camden
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Amsterdam) with: Terrence Ryan@tpryan ) , David Huselid , Alison Huselid , Claude Englebert@cfemea ) , and Ray Camden@cfjedimaster )

Playing With Recursive Components In Angular 6.1.10

By Ben Nadel on

This past week, I started noodling on how to use recursion in an Angular application. And, instead of reaching directly for a component-based solution, I first took at look at implementing recursive views using the Ng-Template directive. Ng-Template is fairly flexible; and, it allows for a recursive view to be managed completely within a single component definition. But, it's not the cleanest code and it has some limitations. As such, I wanted to revisit the challenge of recursion using Angular components.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Whether we use the Ng-Template directive for recursion or we use Components for recursion, the mechanics are essentially the same. In both cases, we have to provide a point of initiation and a point of recursive invocation. In the previous demo, both of those responsibilities were implemented using the Ng-Template directive. In this demo, these responsibilities will be spread across two different components.

First, let's create a TreeComponent. This component will represent the ingress to the Tree widget and will provide Input and Output hooks for the calling context:

  • // Import the core angular services.
  • import { ChangeDetectionStrategy } from "@angular/core";
  • import { Component } from "@angular/core";
  • import { EventEmitter } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • export interface TreeNode {
  • label: string;
  • children: TreeNode[];
  • }
  •  
  • @Component({
  • selector: "my-tree",
  • inputs: [ "rootNode", "selectedNode" ],
  • outputs: [ "selectEvents: select" ],
  • changeDetection: ChangeDetectionStrategy.OnPush,
  • styleUrls: [ "./tree.component.less" ],
  • template:
  • `
  • <my-tree-node
  • [node]="rootNode"
  • [selectedNode]="selectedNode"
  • (select)="selectEvents.emit( $event )">
  • </my-tree-node>
  • `
  • })
  • export class TreeComponent {
  •  
  • public rootNode: TreeNode | null;
  • public selectedNode: TreeNode | null;
  • public selectEvents: EventEmitter<TreeNode>;
  •  
  • // I initialize the tree component.
  • constructor() {
  •  
  • this.rootNode = null;
  • this.selectedNode = null;
  • this.selectEvents = new EventEmitter();
  •  
  • }
  •  
  • }

As you can see, this component does almost nothing. Its primary job is to provide a point of initiation for the recursion. In this case, it does so by rendering the root node, which will then render its own children, each of which will render their own children, and so on.

The TreeComponent provides two inputs:

  • rootNode
  • selectedNode

... and one output event stream:

  • select

Since the node selection may happen at any point within the Tree widget, we have to pass our inputs down through the recursive rendering. And, conversely, we have to propagate any selection event back up through the recursive rendering. On its face, this sounds tedious; but, when you look at the code, you can see that this requires almost no effort: inputs simply become element properties and events get piped directly into EventEmitters.

And, since the rendering of this TreeComponent is based completely on its inputs and outputs, we can switch over to a more efficient change detection strategy: OnPush. This change detection strategy will avoid local digests unless the inputs or outputs change.

Other than providing the hooks for the calling context, the only other thing the TreeComponent does is initiate the recursive rendering using the TreeNodeComponent:

  • // Import the core angular services.
  • import { ChangeDetectionStrategy } from "@angular/core";
  • import { Component } from "@angular/core";
  • import { EventEmitter } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { TreeNode } from "./tree.component";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-tree-node",
  • inputs: [ "node", "selectedNode" ],
  • outputs: [ "selectEvents: select" ],
  • host: {
  • "[class.selected]": "( node === selectedNode )"
  • },
  • changeDetection: ChangeDetectionStrategy.OnPush,
  • styleUrls: [ "./tree-node.component.less" ],
  • template:
  • `
  • <a (click)="selectEvents.emit( node )" class="label">
  • {{ node.label }}
  • </a>
  •  
  • <div *ngIf="node.children.length" class="children">
  •  
  • <ng-template ngFor let-child [ngForOf]="node.children">
  •  
  • <my-tree-node
  • [node]="child"
  • [selectedNode]="selectedNode"
  • (select)="selectEvents.emit( $event )">
  • </my-tree-node>
  •  
  • </ng-template>
  •  
  • </div>
  • `
  • })
  • export class TreeNodeComponent {
  •  
  • public node: TreeNode | null;
  • public selectedNode: TreeNode | null;
  • public selectEvents: EventEmitter<TreeNode>;
  •  
  • // I initialize the tree node component.
  • constructor() {
  •  
  • this.node = null;
  • this.selectedNode = null;
  • this.selectEvents = new EventEmitter();
  •  
  • }
  •  
  • }

Just like the TreeComponent, the TreeNodeComponent provides inputs and outputs for the consuming context. And, similar to the TreeComponent, the TreeNodeComponent must propagate both those inputs and outputs. The primary difference is that the TreeNodeComponent re-renders itself as it iterates over the current node's children. This is the recursive magic in action!

And, just as with the TreeComponent, propagating the inputs and outputs is fairly simple: inputs become element properties and outputs get piped directly into EventEmitters. There is basically no independent state; which means that the TreeNodeComponent can also be made to use the OnPush change detection strategy.

Now that we have the two Angular components that provide the necessary parts of the recursion view rendering, let's package them up in an NgModule so that they can be consumed by the application:

  • // Import the core angular services.
  • import { CommonModule } from "@angular/common";
  • import { NgModule } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { TreeComponent } from "./tree.component";
  • import { TreeNodeComponent } from "./tree-node.component";
  •  
  • // Export the module data structures.
  • export { TreeNode } from "./tree.component";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • CommonModule
  • ],
  • exports: [
  • // The root-level tree component is the only component that an external context
  • // should be able to consume. We don't want people to break encapsulation and
  • // try to use the TreeNodeComponent directly.
  • TreeComponent
  • ],
  • declarations: [
  • TreeComponent,
  • TreeNodeComponent
  • ]
  • })
  • export class TreeModule {
  • // ...
  • }

This TreeModule then gets imported into our AppModule:

  • // Import the core angular services.
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { AppComponent } from "./app.component";
  • import { TreeModule } from "./tree/tree.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • BrowserModule,
  • TreeModule
  • ],
  • declarations: [
  • AppComponent
  • ],
  • bootstrap: [
  • AppComponent
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

... which makes the "my-tree" element directive available to the AppComponent.

Since the TreeComponent and the TreeNodeComponent are both "stateless" (or "dumb") components, the state has to be managed in the calling context. In this demo, that's the AppComponent. And, as you can see below, the AppComponent holds the data structure for the Tree; it holds the reference to the selected node; and, it translates "select" events into state mutations:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { TreeNode } from "./tree/tree.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <my-tree
  • [rootNode]="tree"
  • [selectedNode]="selectedTreeNode"
  • (select)="handleSelection( $event )">
  • </my-tree>
  • `
  • })
  • export class AppComponent {
  •  
  • public tree: TreeNode;
  • public selectedTreeNode: TreeNode | null;
  •  
  • // I initialize the app component.
  • constructor() {
  •  
  • this.selectedTreeNode = null;
  • this.tree = {
  • 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 handle the selection event from the tree component.
  • public handleSelection( node: TreeNode ) : void {
  •  
  • this.selectedTreeNode = node;
  •  
  • console.group( "Selected Tree Node" );
  • console.log( "Label:", node.label );
  • console.log( "Children:", node.children.length );
  • console.groupEnd();
  •  
  • }
  •  
  • }

This demo is more verbose and has more moving parts when compared the Ng-Template approach. But, each part is, in and of itself, quite focused and straightforward. This approach is also quite a bit more flexible since we are not constrained by the limitations of the Ng-Template API.

That said, if we run this in the browser, and select a few TreeNode's, we get the following output:


 
 
 

 
 Rendering recursive views using components in Angular 6.1.10. 
 
 
 

As you can see, we were able to render an arbitrarily-nested tree structure using the TreeComponent and its recursive child, TreeNodeComponent. Each of these components propagated Inputs and Outputs that allowed all of the state to be managed holistically by the AppComponent.

One curious thing that I noticed while writing this was that the recursive TreeNodeComponent instances were not given unique attributes for CSS simulated encapsulation:


 
 
 

 
 Recursive components are all given the same simulated encapsulation ngcontent attributes in Angular 6.1.10. 
 
 
 

As you can see, each encapsulated view for the recursive TreeNodeComponent receives the same [_ngcontenet-c2] attribute. What this means is that some of my CSS logic has to include the "direct descendant" selectors (>) in order to not have CSS styling trickle down through an entire branch of the Document Object Model (DOM) tree. This feels a little "buggy" to me; but, I don't know enough about the simulation logic to say one way or the other.

Recursion is not a tool that you have to reach for a whole lot. But, when you need it, it's often the only sane and manageable solution. And, it's nice to see that Angular 6.1.10 provides recursive solutions for arbitrarily nested data structures that include both Ng-Template and Component-based mechanics. Hopefully this post sheds some light on the relatively straightforward way that data (inputs) and events (outputs) can be propagated up and down a recursive DOM structure.



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,

So, yesterday when I was experimenting with this stuff, I said that the nghost and ngcontent behavior "seemed buggy". But, that was because I didn't really have a solid grasp on how emulated encapsulation worked in Angular. As such, I needed to step back and sort out my mental model:

https://www.bennadel.com/blog/3514-emulated-encapsulation-host-and-content-attributes-are-calculated-once-per-component-type-in-angular-6-1-10.htm

What I codified in my mind was that the emulated encapsulation is calculated per component type - not per component instance. As such, it makes sense that all instances of the same component will have the same nghost and ngcontent attributes; which, in turn, explains why the nested my-tree-node styling worked.

Reply to this Comment

Just a question, I'm curious, why you prefer typing inputs outputs and binding in the host declaration of the component. I read somewhere that is not recommended, or at least it is quite limiting. Instead I'm using massively the decorators provided @Input, @Output, @HostBinding etc.

Reply to this Comment

And looking at the other post you shared in the comments, my 2 cents is - I always put my styles in :host {}, not sure if this helps for your case.

Reply to this Comment

@Zlati,

Good question. For me, I feel like the Inputs, Outputs, and Host bindings make more sense in the @Component() because they are telling Angular how the Component relates to the View, not how the Component class works. This way, all the logic for the *class is inside of the Class; and, all the meta-data about how it relates to the view is inside the meta-data.

After all, from a Class stand-point, the Inputs and Outputs don't mean anything. They are nothing put public properties. Inputs and Outputs are only meaningful in the way that Angular connects the class instance to the View. So, I keep it at the top, to make it easier to see where all the meta-data is.

As far as "limiting", they should be functionality equivalent. I don't believe there is anything you can do with an inline decorator that you can't do with the @Component() meta-data; unless you were thinking of something specific?

Reply to this Comment

I guess, I never bothered to change the tslint config from angular cli. The property use-host-property-decorator is true and that's why https://angular.io/guide/styleguide#style-06-03

I also had some issues with animations when using the animations setting. So I had to rewrite and use the Animation Builder.
Generally I think it's ok for static stuff only, in the moment you want getter or something else dynamic you need the decorators.

Reply to this Comment

@Zlati,

I agree with a lot of the style-guide stuff. But, I also disagree with some of it (like this stuff). The underlying theme in the style-guide is just "be consistent". As long as you find something you are comfortable with and stick to it, I think that's the most important part.

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.