A Tree-View Angular Component Tale— Part 2: Nested Information

Having accomplished to list the contents of our local root directory, it is time to start thinking how to make our tree-view component capable of displaying nested levels of directory contents. In this part we will consider a few different architectural options to organise and share this nested information, progressively reaching a solution that fits our Files application the best.

← Read Part 1: Building Foundations

Representing Nested Information

This time, we will start by restructuring our State interface first in order to handle nested directory contents. Remember how we structured it in part 1:

We just needed a flat Node array to store the root directory contents. Now, since some of these nodes are actually directories that may possibly contain other contents, we are going to need a new way to store this information.

Since each node may contain other child nodes, it makes a lot of sense to change the very definition of the Node interface to include an optional children array:

Now in order to store child nodes, all we have to do is change fsReducer to search for the parent directory using the uri property of each node and assign the contents to children. But as we start implementing this algorithm, we realise that it is somehow more complicated than it sounds. We need to search an arbitrary number of nested levels to find the parent node and each time we reach a dead-end following one path, we need to go back and try a different one. Essentially, we need to perform a tree traversal algorithm. Surely possible, but what if we can do something speedier and avoid all the fuss?

Instead of having an endless nested hierarchy of nodes, we can store the contents of each directory in a flat, simple data structure. We are essentially going to create a hash map: the unique directory paths are going to be used as as keys for each entry and each entry is going to hold the corresponding directory contents as an array of nodes. This way, we can leave our Node interface untouched and we are going to restructure our State instead:

We reduce the new state using the directory path to create a new entry or replace the value of some entry in the new flat structure

Now, on every [File System API] Directory Contents action, we use the path argument that we had included in the payload to create or replace the contents of that specific entry in our hash map (which is nothing more than a simple javascript object). Great! We avoided having to deal with all the nested madness that is inherent to a tree structure by coming up with a way to represent the information in a flat manner. Let’s create a new selector to help us select only the contents of the directory we are interested in:

If we now use the new selector in ngOnInit of app.component.ts to retrieve the nodes under the '/' path everything should work as before:

this.nodes$ = this.store.pipe(select(selectPath, { path: '/' }));

State-Aware Components

Ok, we now need to introduce some changes to our TreeViewComponent and TreeNodeComponent in order to be able to view the nested directory contents. We can use these two components recursively by including a <files-tree-view> tag in TreeNodeComponent‘s HTML template. Of course, TreeViewComponent expects an array of nodes as an input. But since we decided to go with the flat structure and nodes do not themselves store a reference to their child nodes array, it is impossible to pass down this type of information to the nested tree-view using pure Angular mechanics. Thankfully, we have yet another way of sharing information between the application components; I am, of course, talking about the Store.

The way things have been set up until now, AppComponent is responsible both for dispatching the initial event to get the contents of the root path as well as listening for the contents from the server and passing it down to TreeViewComponent. We can liberate AppComponent from the latter by injecting the Store directly into the TreeViewComponent:

This way it becomes directly responsible for retrieving the information it needs. Notice that we need to pass down the path, the contents of which we want to retrieve and list in the TreeViewComponent. Let’s change AppComponent to use TreeViewComponent correctly and also clean it up a bit:

It becomes very easy now to start displaying nested levels of directory contents. Each TreeNodeComponent can contain a TreeViewComponent in its template and pass down the node’s uri property as the path. When the user clicks on the node, it will dispatch an action to fetch the contents of the directory under that path and the inner tree-view will be rendered.

We wrap each .node-name element in another div to handle user clicks and we also update the styles to intend nested contents and change the arrow character when a node is extended. Refreshing the application in the browser now hopefully renders your local root directory contents as before and clicking on an directory node for which you have permission to access should cause it to expand (we are not going to handle errors for now), retrieve and then list its contents in a nested tree-view!

Directory structure and contents of Files, viewed using the very same application!

Nested Components Revised

You may have noticed that we made some unorthodox decisions when it came to nesting our components. Typically, when we think about trees as a data structure, it is not very common for a node to contain a reference to another tree. Instead, we think of trees as a nested structure of nodes. We can refactor TreeNodeComponent so that it only nests children of itself:

Now, instead of just dispatching an action on user click, each node also listens for children nodes on its path. We can also go a step further and change TreeViewComponent to just be a wrapper around the top level node. We will need to pass down the root directory to this top TreeNodeComponent; we could do this directly inside the TreeViewComponent but it would be more appropriate to include it in the application state, in case our application expands and we want to be able to change the top level directory by dispatching an action.

Let’s also create a new selector that will give us the root directory (append at the end of fs.selectors.ts):

Now TreeViewComponent can retrieve the root directory node from the Store and simply pass it down to the top level TreeNodeComponent:

With this we have completely incorporated all the logic inside our tree-view and tree-node components. In fact, you can go ahead and completely scrap the body of AppComponent and just include the <files-tree-view> tag in the template without any inputs!

A wild ‘/’ node appeared.

You have probably noticed that the top-level directory now has its own expandable node at the top of the tree-view. You could argue if this is a necessary feature or not, but we could easily include an option to hide it if we wanted to.

We have come far enough and it seems that we already have a pretty functional tree-view component, capable of displaying nested directory contents on demand! In the next part we will go through some refactoring and eliminate the dependencies of different parts of the component on application-wide elements (like the application state) so that we could ship it as a stand-alone component that other applications could incorporate and use as well. Basically, we will take a few steps back and think of another way to use our Store in order to favor reusability.

Part 3: Reusability →

Front-end developer, science nerd, free spirit.