DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Exploring Intercooler.js: Simplify AJAX With HTML Attributes
  • Mastering SSR and CSR in Next.js: Building High-Performance Data Visualizations
  • LLMops: The Future of AI Model Management
  • Transforming Customer Feedback With Automation of Summaries and Labels Using TAG and RAG

Trending

  • DGS GraphQL and Spring Boot
  • Automating Data Pipelines: Generating PySpark and SQL Jobs With LLMs in Cloudera
  • Beyond Simple Responses: Building Truly Conversational LLM Chatbots
  • A Simple, Convenience Package for the Azure Cosmos DB Go SDK
  1. DZone
  2. Data Engineering
  3. Data
  4. Recursive Angular Rendering of a Deeply Nested Travel Gallery

Recursive Angular Rendering of a Deeply Nested Travel Gallery

By 
Alexander Eleseev user avatar
Alexander Eleseev
DZone Core CORE ·
Aug. 31, 20 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
13.0K Views

Join the DZone community and get the full member experience.

Join For Free

Suppose you like to travel and have collected a large photo gallery. The photos are stored in a tree folder structure, where locations are structured according to the geography and administrative division of a country:

Tree folder structure

The actual photos of particular places are stored in the corresponding leafs of the tree. Different branches of the tree may have different height. You want to show these photos in your portfolio website that is made on Angular. Also, the gallery should be easily extendible with new locations and photos.

The Problem

This problem statement is similar to that of my previous post. To show this gallery, we need two Angular libraries: angular-material-tabs and ivy-carousel or their equivalents. The tabs library provides a tree structure for the gallery locations, while the carousel shows the photos of a particular place.

Let's take a look at a simple tabs example:

HTML
 




xxxxxxxxxx
1


 
1
<mat-tab-group>
2
  <mat-tab label="First"> Content 1 </mat-tab>
3
  <mat-tab label="Second"> Content 2 </mat-tab>
4
  <mat-tab label="Third"> Content 3 </mat-tab>
5
</mat-tab-group>



Here the tabs are grouped within the <mat-tab-group> tag. The content of an individual tab is wrapped in the <mat-tab> tag, while the tab title is the label value.

To add an extra level of tabs, one needs to insert a whole <mat-tab-group> block into an appropriate <mat-tab>. Clearly, such construct becomes very cumbersome and hard to extend as the gallery becomes sufficiently deep. So we need to automatically inject Angular template or component into the <mat-tab> elements and automatically pass the appropriate title to the <mat-tab label="title"> element.

Angular carousel works as follows:

HTML
 




xxxxxxxxxx
1


 
1
<carousel>
2
    <div class="carousel-cell">
3
        <img src="path_to_image">
4
    </div>
5
    <div class="carousel-cell">
6
        ...
7
</carousel>



The carousel content is wrapped into the <carousel> tag. Each individual image is wrapped into a <div class="carousel-cell"> tag. To show a large number of images in a tree folder structure, we need to automatically construct and provide a full folder path to every image into the src field.

Let's see how to solve these problems in Angular.

The Solution

Firstly, there is a convenient data structure for this problem. Secondly, I demonstrate 3 separate approaches to render the data structure in Angular: a recursive template approach, a recursive component approach, and a recursive approach  with a single mutable shareable stack. The first two approaches are inspired by the post by jaysermendez. The last approach doesn't duplicate data and illustrates the difference between the Angular and React change detection mechanisms. The code for all 3 approaches is here.

The Data Structure

Let's pack our data to the following data structure:

JavaScript
 




xxxxxxxxxx
1
29


 
1
data =[
2
    {name:'Russia',
3
      children:[{name:'Vladimir Area',
4
      children:[
5
        {name:'Vladimir',
6
        children:[
7
                {title:"Uspenskii Cathedral",
8
                 screen:"uspenski.JPG",
9
                 description:"The cathedral was built in 12th century by Duke Andrey Bogoliubov."},
10
                {title:"Saints of Dmitrov Cathedral",
11
                 screen:"saints.JPG",
12
                 description:"Saints of Dmitrov cathedral. The cathedral was built in 12th century by Duke Andrey Bogoliubov."},
13
              ]
14
            },
15
        {name:'Bogoliubovo',
16
        children:[{...},{...}]}
17
        ]
18
      },
19
      {name:'Moscow Area',
20
      children:[...]
21
      }
22
    ]
23
  },
24
  {
25
    name:'Spain',
26
    children:[...]
27
  },
28
  {...}
29
];



This recursive tree pattern has nodes:

JavaScript
 




xxxxxxxxxx
1


 
1
{ name:'Name1',
2
  children:[
3
    {name:'Sub Name1',children:[...]}
4
  ]
5
}



 and leafs:

JavaScript
 




xxxxxxxxxx
1


 
1
{ title:'Title1',
2
  screen:'screen1.png',
3
  description:'Description 1'
4
}



Angular code for this data structure can be generated in a top-down recursive manner. All the grammar, production rules, and compiler theory arguments from my previous post are applied here as well. The Angular specific thing is how to pass the proper folder paths to each template or component.

Approach 1: Recursive Template

For this approach we need a node and a leaf templates. For the details look at this post. I focus on how to pass folder paths to the templates. So, the node template is:

HTML
 




xxxxxxxxxx
1
18


 
1
  <ng-template #treeView let-data="data" let-path="path" >
2
    <mat-tab-group>
3
    <mat-tab  *ngFor="let item of data" label="{{item.name}}">
4
        <div *ngIf="item[this.key]?.length > 0 && item[this.key][0]['name'];
5
         else  elseBlock">    
6
         <ng-container        
7
            *ngTemplateOutlet="treeView;
8
            context:{ data: item[this.key] , path:path.concat(item.name)} ">
9
        </ng-container>
10
        </div>
11
    <ng-template #elseBlock>
12
      <ng-container  *ngTemplateOutlet="leafView;
13
   context:{data:item[this.key] ,leafPath:path.concat(item.name).join('/')}  " >
14
      </ng-container>
15
    </ng-template>
16
   </mat-tab >
17
  </mat-tab-group>
18
  </ng-template>



This template accepts data (in the form described above) and path as input parameters. The this.key="children" is taken from the component; only 1 component is needed in this approach. At lines 4-5 the system determines if the child is a node, or a leaf. In the former case the system calls the treeView template again with item['children'] at lines 6-9. In the later case calls the leafView  template with item['children'] at lines 11-15.

The leaf template is 

HTML
 




xxxxxxxxxx
1


 
1
<ng-template #leafView let-data="data" let-path="leafPath" >
2
  <carousel>
3
    <div class="carousel-cell" *ngFor="let item of data">
4
      <img [src]="'assets/images/'+path+'/'+item.screen">
5
  </div>
6
  </carousel>
7
</ng-template>



Notice how the path is passed to the template. Since every Angular template has its own scope, we concatenate the previous path with the item.name. So, every template receives its own copy of the path and some of the path strings get duplicated in every node and leaf. There is no path.pop() anywhere.

The recursive process is initiated as

HTML
 




xxxxxxxxxx
1


 
1
<ng-container *ngTemplateOutlet="treeView; context:{ data: data,path:this.pathStack} "></ng-container>



Here the data is our data structure and the this.pathStack=[].

Approach 2: Recursive Component

A very similar approach to the previous one. Here we replace the node template with a node component (see the post and the code for details). The component's view is

HTML
 




xxxxxxxxxx
1
21


 
1
<ng-template #leafView  let-data="data" let-path="leafPath" >
2
   <carousel >
3
      <div class="carousel-cell" *ngFor="let item of data">
4
      <img [src]="'assets/images/'+path+'/'+item.screen">
5
  </div>
6
  </carousel>
7
</ng-template>
8

          
9
<mat-tab-group>
10
  <mat-tab  *ngFor="let item of this.items" label="{{item.name}}">
11
    <div *ngIf="item[this.key]?.length > 0 && item[this.key][0]['name'];
12
                else elseBlock">
13
     <tree-view *ngIf="item[this.key]?.length" [key]="this.key" [data]="item[this.key]" [path]="this.path.concat(item.name)"></tree-view>
14
     </div>
15
    <ng-template #elseBlock>
16
      <ng-container  *ngTemplateOutlet="leafView;  
17
           context:{ data: item[this.key],                                           leafPath:this.path.concat(item.name).join('/')} " >
18
      </ng-container>
19
  </ng-template> 
20
  </mat-tab >
21
</mat-tab-group>



where the leaf template is already included. The component's controller is:

JavaScript
 




xxxxxxxxxx
1
11


 
1
@Component({
2
  selector: 'tree-view',
3
  changeDetection: ChangeDetectionStrategy.OnPush,
4
  templateUrl: './tree-view.component.html',
5
  styleUrls: ['./tree-view.component.css']
6
})
7
export class TreeViewComponent {
8
  @Input('data') items: Array<Object>;
9
  @Input('key') key: string;
10
  @Input('path') path: Array<Object>;
11
 }



No life cycle hooks are needed. Ones again the old path get concatenated with the item['name'] to get the new path. 

The recursion starts as 

HTML
 




xxxxxxxxxx
1


 
1
<tree-view [data]="data" [key]="this.key" [path]="this.pathStack"></tree-view>



where the data is the data structure, key="children", pathStack=[].

Every node component stores its copy of the path in the this.path variable. Is there a way not to duplicate the path data in node components?

Approach 3: Recursive Components With a Single Shareable Stack  

For this approach we need 2 components (for the node and leaf)  and 1 service (to store and modify a stack). The challenge here is how to use the component's life cycle hooks to update the stack and to properly use the Angular change detection mechanism to provide the right stack to the right leaf.

The stack service (a singleton by default) is very simple:

JavaScript
 




xxxxxxxxxx
1
17


 
1
@Injectable({
2
  providedIn: 'root'
3
})
4
export class StackService {
5
  stack: Array<String> =[]
6
  constructor() { }
7
  getStack(){
8
    console.log('stack: ',this.stack)
9
    return this.stack;
10
  }
11
  popStack(){
12
    this.stack.pop();
13
  }
14
  pushStack(path){
15
    this.stack.push(path);
16
  }
17
}



The stack is stored as an array of strings. The service provides the push/pop and get operations on the stack.

The node component controller is:

JavaScript
 




xxxxxxxxxx
1
20


 
1
import { Component, OnInit, Input, ChangeDetectionStrategy, AfterViewInit} from '@angular/core';
2
import {StackService} from '../services/stack.service';
3
@Component({
4
  selector: 'tree-view-stack',
5
  templateUrl: './tree-view-stack.component.html',
6
  styleUrls: ['./tree-view-stack.component.css']
7
})
8
export class TreeViewStackComponent implements OnInit, AfterViewInit {
9
  @Input('data') items: Array<Object>;
10
  @Input('key') key: string;
11
  @Input('path') path: Array<Object>;
12
  
13
  constructor(private stackService:StackService) { }
14
  ngAfterViewInit(): void {
15
   this.stackService.popStack();
16
  }
17
  ngOnInit() {
18
  this.stackService.pushStack(this.path);
19
    }
20
}



The component receives the data array, path, and key string as inputs. The StackService is injected into the constructor. We use 2 component life cycle hooks to update the stack. A child folder name is pushed to the stack by the ngOnInit() hook. This hook is called only ones after the component initializes and receives its inputs from its parent component.

Also, we use the ngAfterViewInit() hook to pop the stack. The hook is called after all the child views are initialized. This is a direct analogue of pathStack.pop()of recursive JSX rendering, described in my previous post.

The node component view is quite simple:

HTML
 




xxxxxxxxxx
1
11


 
1
<mat-tab-group>
2
  <mat-tab  *ngFor="let item of this.items" label="{{item.name}}">
3
     <div *ngIf="item[this.key]?.length > 0 && item[this.key][0]['name'];else elseBlock">
4
     <tree-view-stack *ngIf="item[this.key]?.length" [key]="this.key" [data]="item[this.key]" [path]="item.name"></tree-view-stack>
5
     </div>
6
    <ng-template #elseBlock>
7
     <tree-leaf-stack [data]="item[this.key]" [key]="this.key" [path]="item.name">
8
    </tree-leaf-stack>
9
  </ng-template>  
10
  </mat-tab >
11
</mat-tab-group>



Ones again, the system chooses what component, a node or a leaf, to render next. No path concatenations this time though.

The leaf component controller is 

JavaScript
 




xxxxxxxxxx
1
18


 
1
import { Component, Input,OnInit, ChangeDetectionStrategy } from '@angular/core';
2
import {StackService} from '../services/stack.service';
3
@Component({
4
  selector: 'tree-leaf-stack',
5
  templateUrl: './tree-leaf-stack.component.html',
6
  styleUrls: ['./tree-leaf-stack.component.css']
7
})
8
export class TreeLeafStackComponent implements OnInit {
9
  @Input('data') items: Array<Object>;
10
  @Input('key') key: string;
11
  @Input('path') path: String;
12
  
13
  fullPath: String;
14
  constructor(private stackService:StackService) { }
15
  ngOnInit() {
16
    this.fullPath = this.stackService.getStack().concat(this.path).join('/');
17
  }
18
}



Here I cut the corner and don't use the ngAfterViewInit()hook to pop the stack. The this.fullPath variable gets computed ones in the ngOnInit()hook as the leaf component initializes. Finally, the leaf view is 

HTML
 




xxxxxxxxxx
1


 
1
  <carousel >
2
    <div class="carousel-cell" *ngFor="let item of items">
3
      <img [src]="'assets/images/'+fullPath+'/'+item.screen">
4
  </div>
5
  </carousel>



The key question here is why we can't call this.stackService.getStack() directly in the template instead of first saving the call result in the fullPath variable and then interpolate the variable {{...+fullPath+...}}?

The reason is how Angular detects changes. Whenever the variables in the stackServce mutate, the onChange() component life cycle hook triggers in every component that calls the service. This happens since Angular sets a watcher on every interpolation {{...}}. So, whenever the stack gets pushed or popped, the {{this.stackService.getStack()}} would be called in every template of every component where there is such an interpolation. 

First, this would mean that all the templates get the same value (an empty string) of the stack after the DOM is fully rendered. Second, it would greatly slow down the browser since there will be a lot of calls.

The ChangeDetectionStrategy.OnPush doesn't help in this case since the strategy only addresses the @Input parameters. The strategy prevents updates if the references to an input parameter object remains the same while the fields of the object get mutated.

React on the other hand detects changes differently. A React component is re-rendered if the component's props change, setState(...) method is called, or shouldComponentUpdate(...) is called. If any of these happen, the component just calls its render() method to emit JSX code. There are no watchers inside the render() method. So, this explains the difference between how the recursive rendering problem is implemented in Angular and in React.

The Results 

All 3 approaches give the same results:

Final results

The first approach is the fastest to run since it doesn't initialize a full-fledged component on every recursion step. The third approach doesn't duplicate the path data in every node component.  

Conclusions

In this post I demonstrated 3 approaches to recursively render a deeply nested travel gallery in Angular. The third approach illustrates how the change detection mechanism works in Angular and how it differs from that of React. Never call methods directly from an Angular template!  

The code for all 3 approaches is here.

AngularJS Template Data structure Travel Data (computing) Hook HTML

Opinions expressed by DZone contributors are their own.

Related

  • Exploring Intercooler.js: Simplify AJAX With HTML Attributes
  • Mastering SSR and CSR in Next.js: Building High-Performance Data Visualizations
  • LLMops: The Future of AI Model Management
  • Transforming Customer Feedback With Automation of Summaries and Labels Using TAG and RAG

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!