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:

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:

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

This recursive tree pattern has nodes:

 and leafs:

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:

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 

Notice how the path is passed to the template. Since every Angular template has its own scope, we concatenate the previous path with the 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

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

0 && item[this.key][0][‘name’];
else elseBlock”>

” data-lang=”text/html”>

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

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 

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:

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:

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:

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 

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 

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.  


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.

Source link

Write A Comment