A Tree-View Angular Component Tale— Part 1: Building Foundations
In this part we will set our specifications and develop a high-level plan of how to execute the development of the project. Next, following our plan, we will set up a simple application to host the tree-view component and we will have it display our local root directory contents using a simple server written in Node.
Concept & Specifications
We need our tree-view to be capable of doing two main things:
- List all child nodes of a predefined root node.
- List nested child nodes of parent nodes when they are expanded (provided they contain such nodes).
You could argue that at a basic level, these are not really two distinct functions and we could sum up the functionality to just “recursively list all child nodes of a parent node, if it has any”. It makes it a lot easier though, not only for the purposes of this story, but for the whole development process to set these two distinct milestones. In this part, we will focus on completing the first one. Hopefully, as you follow along with the story, the reasons for this separation of functions will start becoming more and more apparent.
We will use a dead-simple Node server running locally to retrieve the contents of directories and display them using our component. I believe that a bold disclaimer is necessary here: I attempt to build a Node server for the first time, having glued together different pieces of information I found online. Its sole purpose is just to fulfil the needs of and support the main project we present in this story and not to be considered part of it. And, well, for its purpose, it works.
Throughout the whole process, we will be designing and building our tree-view component to be a part of an application called Files. This will give us a platform to start crafting our component and found the base for reusability. At a later moment, we will also assume that other developers may need to use the component in their own projects. This will force us to create a clear public interface and eliminate its potential dependencies on Files, rendering it completely reusable.
It would be good to come up with a high-level plan for how to proceed with the development of the project now. First, we will set up the application and get it ready to host our tree-view component. Then, we will create the component and make sure it can correctly display a flat list of nodes with mock data. Notice that this natural milestone is well in line with the first of the two main functionalities we defined in the beginning of this section. Next comes the interesting part of introducing NgRx. We will build actions, effects and reducers to populate our application state with real data coming from the server and selectors to access this data and consume it in our component. Finally, we will extend the functionality to make it possible to list nested child nodes. A lot of different interesting architectural options will show up during this phase and we will have to reason a little bit about which one to pick as our best choice. We are not going to try to over-design everything from the beginning though, but we are instead going to try some solutions and see what works best for us, since I want the whole development process to be progressive and educational, provoking us to learn from the different situations that arise in each case. As we reach the end, we will go the extra mile and include an input to give us the ability to type in a path and change the root node of the tree view (maybe?).
Having all that in mind, it is time to get hands-on with the project!
Application Set-Up
Let’s create a new Angular application using Angular’s command line interface. Create a new directory for the project, navigate to it and type
ng new files-app --minimal --prefix files --routing false --style css
Setting the --minimal
flag, we avoid using a testing framework (since we are not going to focus on testing for the purposes of this project) and keep the styles and template strings inline in the components. --prefix
is another practical option which automatically sets the prefix of all component names to whatever we tell it to (defaults to “app”). We set --routing
to false
since we are also not going to use it. Lastly, we set the --style
parameter to just plain css, since we are going to write the styles inline to the component and any preprocessing of inline styles is currently not supported by Angular. If you prefer to split styles into different autonomous files and use another preprocessor, feel free to change the option to match your likings.
Great, Angular should do its magic and provide us with a new directory called files-app containing all generated project files. Executing ng serve
from within this directory should start the Angular server and you should be able to access the application on localhost:4200
.
We should also start the Node server since we are going to need it by the end of this article. Supposing you are in whatever directory you downloaded the server files from GitHub (package.json
and serve.js
), you should type npm install
to install dependencies. When this is done you can start the server with node serve.js
. You can reach the server on localhost:8080
. I suggest you leave it running in a separate terminal tab as you follow along with the story.
The Flat Tree-View Component
We are going to go ahead and create our initial version of the tree-view component:
ng g component tree-view --inlineStyle --inlineTemplate --skipTests
Again, as with every file we are going to generate, we are keeping the styles and template inline for ease of presentation and also skipping the tests. Angular creates the component under its own dedicated directory called tree-view
and updates app.module.ts
to import it. Let’s go ahead and change the template in app.component.ts
to show the newly created component and verify it works.
Now refreshing the application in the web browser should just display the default string “tree-view works”. We ultimately want our tree-view component to list nodes, so let’s go ahead and create tree-node.component.ts
under the same directory:
If you created the component manually, don’t forget to declare it in the AppModule
which should now look like this:
Our application starts taking some structure, but what type of information will a TreeNodeComponent
need to display?
Node Interface
The Node server we are going to use to retrieve directory contents from our local file system is going to return a list of nodes in JSON format, each with three string properties, uri
, name
and type
. Let’s go ahead and create a compatible Typescript interface so that we can use it throughout the application. We are going to do it manually this time since Angular CLI is not particularly useful in this case. We are going to create a models
directory under app
and a new file called node.ts
inside it.
We already declared a NodeType
type consisting of all possible strings that the server could return as a node type. Let’s now use our Node
interface to indicate the type of input data our components expect, and furthermore try to display the data in the template. We are going to give “dir” type nodes their distinctive right-pointing arrow to indicate that they can be expanded:
Let’s feed in the component with some mock data to quickly test if it is working:
The browser window reloads and we get our first look of our tree-view component!
Let there be State
We obviously want to utilise the Node server to retrieve and display the contents of our local file system. Mock data is not satisfying enough and, well, the whole story would not have a reason of existence. Assuming that Files is going to get big enough and will accommodate more than just our tree-view component, we will introduce immutable state into our application in order to handle how and when the different views update.
Installing NgRx
We are going to install two components of NgRx:
- @ngrx/store to manipulate immutable state through actions and reducers and
- @ngrx/effects to produce side-effects from actions that in turn dispatch other actions.
Angular CLI provides a pretty easy way to do this:
ng add @ngrx/store --minimal
ng add @ngrx/effects --minimal
Always make sure to execute these commands from within the application’s directory. The --minimal
flag installs the components making no changes to our application structure: no new directories or files, just updating app.module.ts
to import StoreModule
and EffectsModule
. We are going to build the state management completely from scratch.
Consuming The REST API
Let’s use Angular CLI once more to create a service that will be used to consume the REST API provided by the Node server:
ng g service fs-api --skipTests
A new file called fs-api.service.ts
is created under our app
directory (“fs” stands for file system). We are going to use this service as a façade for our communication needs with the REST API. Let’s edit the file and add a method that sends an HTTP GET
request to the server to get the contents of a directory:
We are going to use this method in a while when we will be building our NgRx effects.
Creating Actions
For now, we want to be able to retrieve all the nodes of one directory and list them in the tree-view. Let’s think about what events could occur during this process:
- When Files starts, a request is made to retrieve the contents of a directory.
- At some future point in time, we get a response from the server with the directory contents.
We can represent these events with two different actions. Let’s create a folder under app
and call it store
. In there we will create two files for our two actions, since the events that they represent come from two different sources:
We are using the new createAction
function in NgRx 8 and we use props
to define each action’s payload. To retrieve the contents of the directory we, of course, need to know the path of the directory. When the contents arrive, we cast them using the Node
interface we created at the beginning.
Defining State & Reducing The Actions
We need to define a simple state for our store in order to be able to save the directory contents retrieved from the server. Since we are still focusing on listing only one level deep, we are just going to need an array of Node
s. Let’s create another file under store
directory and call it fs.reducers.ts
:
Simply enough, the reducer just takes the nodes coming from the server response and replaces any previous nodes in the store with the new ones. Since we now have the reducer for this feature defined we can go ahead and register it in app.module.ts
. When we installed @ngrx/store
, Angular already imported the StoreModule
for us automatically. We can go ahead and fill in this line to include our reducer:
import * as Fs from './store/fs.reducer';...StoreModule.forRoot({ fs: Fs.reducer }, {
...
})
Let’s also create a selector now in a new file fs.selectors.ts
(always under store
directory) to give us the nodes
slice in the store:
So now we have actions representing events to request and retrieve the contents of a directory. We also have a state defined and a reducer to act upon the event of receiving the contents from the server and save them into the store. So how do we connect all of these?
Creating Side-Effects
We are going to create an effect to be triggered upon the [File System] List Directory Contents
action in a file under store
directory called fs.reducers.ts
:
We use the new createEffect
function in NgRx 8. In the FsEffects
class we inject Actions
and FsApiService
. We pipe the action stream to filter the kind of action upon which we wish to trigger the effect using the ofType
operator and then we perform a bunch of mapping operations that end in some action being dispatched: we call the ls
method we created in FsApiService
to retrieve the contents of a directory from the server and we map the response to the [File System API] Directory Contents
action that will automatically get dispatched. The effect won’t take effect (pun not intended) unless we subscribe our FsEffects
class in app.module.ts
using the EffectsModule
import that Angular has already put there for us:
import { FsEffects } from './store/fs.effects';...EffectsModule.forRoot([FsEffects])
What remains now is to put everything in use to retrieve actual data and use it in place of the manually introduced nodes
array earlier in AppComponent
:
Supposing we have the Node server up and running, the refreshed page on the browser should now present you with the contents of your local root directory, looking something like this:
Hurray! We are on a good way. Of course our tree-view component does not function like a component of its kind should. In the next part of this story, we are going to level up the fun and consider how to make the tree-view display contents of multiple deeper level directories. After trying a few options and learning from the situations that will arise, we will stick with the solution that fits us best for our application.