Jul 29th, 2017 - written by Kimserey with .
PrimeNg is a Angular component library. Compared to other component libraries like ngbootstrap or material, PrimeNg comes with more advance components which can’t be found elsewhere, one of them being the tree structure. Having the component is one thing but having to build the tree data which can be used by the component is another hard part. Therefore today I will firstly show how we can use PrimeNg and secondly I will show how we can mold data to fit in the model used to build PrimeNg tree.
PrimeNg can be added via npm npm install primeng --save
.
It also needs font awesome for icons which can be added via npm npm install font-awesome --save
.
After installed, under the /primeng/resources
folder, we should be able to see the style files. Those needs to be added to the styles in the angularCLI .angular-cli.json
config.
1
2
3
4
5
"styles": [
"../node_modules/font-awesome/css/font-awesome.min.css",
"../node_modules/primeng/resources/primeng.min.css",
"../node_modules/primeng/resources/themes/omega/theme.css"
]
Each component is contained in its own module. In this tutorial we will be using the TreeModule
and the Tree
class.
We start first by importing the module.
1
2
3
4
5
6
7
8
9
10
11
12
import { TreeModule } from 'primeng/primeng';
@NgModule({
imports: [
CommonModule,
FormsModule,
TreeModule
],
declarations: [
PrimeNgComponent
]
})
A tree is constructed using an array of TreeNode
. The selector for the tree is p-tree
. Let’s start by making an example tree.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Component, OnInit } from '@angular/core';
import { TreeNode } from 'primeng/primeng';
@Component({
template: '<p-tree [value]="files"></p-tree>'
})
export class PrimeNgComponent implements OnInit {
files: TreeNode[];
ngOnInit() {
this.files = [
{
label: 'Folder 1',
collapsedIcon: 'fa-folder',
expandedIcon: 'fa-folder-open',
children: [
{
label: 'Folder 2',
collapsedIcon: 'fa-folder',
expandedIcon: 'fa-folder-open',
children: [
{
label: 'File 2',
icon: 'fa-file-o'
}
]
},
{
label: 'Folder 2',
collapsedIcon: 'fa-folder',
expandedIcon: 'fa-folder-open'
},
{
label: 'File 1',
icon: 'fa-file-o'
}
]
}
];
}
}
As mentioned previously, we use the p-tree
selector <p-tree [value]="files"></p-tree>
. TreeNode
has a list of field which can be found on the documentation https://www.primefaces.org/primeng/#/tree. All the fields are optional, here we chose the following:
The result should be as followed:
Tree structures are hard to construct. Especially for file paths, usually what we get is an array of path as followed:
1
2
3
4
5
6
7
[
'folderA/file1.txt',
'folderA/file1.txt',
'folderA/folderB/file1.txt',
'folderA/folderB/file2.txt',
'folderC/file1.txt'
]
In this section we will be using reduce
too construct the file tree using an array of path https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce.
reduce
iterates over every element of the array while constructing a result passed from iterations to interations.
The benefit of it is that we end up with a code which is free from side effect, employing function only needing input and output.
We start fist by writing the skeleton:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class PrimeNgComponent implements OnInit {
files: TreeNode[];
reducePath = (nodes: TreeNode[], path: string) => {
return [];
}
ngOnInit() {
const f = [
'folderA/file1.txt',
'folderA/file1.txt',
'folderA/folderB/file1.txt',
'folderA/folderB/file2.txt',
'folderC/file1.txt'
];
this.files = f.reduce(this.reducePath, []);
}
}
reducePath
takes the previous state TreeNode[]
, which is the result of the previous iteration on the previous path value, and the currenct value it is iterating on. The result of the function is the next state. The second argument of reduce
is the initial value of the state.
Notice that reducePath is defined as a variable, this is needed in order to recursively call itself.
The idea:
As a tree traversal algorithm, we will be:
After that only three possibilities remain:
We start first by handling the first scenario, the path is just a file. If it is just a file, it means that where we reduced to is the correct folder where the file should be. So we add it to the list of nodes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
reducePath = (nodes: TreeNode[], path: string) => {
const split = path.split('/');
if (split.length === 1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-file-o'
}
];
}
// will be removed
return [];
}
If the first piece of the path is a folder, it means that we are still reducing the path. We handle the scenario where the folder does not exist by adding a new folder to the list of nodes.
We know from here that the file will be a child of this newly created folder therefore we reduce the remaining path and set the result to the folder children.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
reducePath = (nodes: TreeNode[], path: string) => {
const split = path.split('/');
if (split.length === 1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-file-o'
}
];
}
if (nodes.findIndex(n => n.label === split[0]) === -1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-folder',
children: this.reducePath([], split.slice(1).join('/'))
}
];
}
// will be removed
return [];
}
Lastly if the folder already exists, we know that the file will be under an existing folder already within the nodes
.
So we iterate over all nodes
and when found, reduce the rest of the path together with the current children of the node.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
reducePath = (nodes: TreeNode[], path: string) => {
const split = path.split('/');
if (split.length === 1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-file-o'
}
];
}
if (nodes.findIndex(n => n.label === split[0]) === -1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-folder',
children: this.reducePath([], split.slice(1).join('/'))
}
];
}
return nodes.map(n => {
if (n.label !== split[0]) {
return n;
}
return Object.assign({}, n, {
children: this.reducePath(n.children, split.slice(1).join('/'))
});
});
}
And that’s it, this should construct the tree with folders, subfolders and files.
The tree construct was actually only a demonstration of the utility of the reduce function. There are other instances where reducing is effective, for example for grouping/categorizing, it is very useful to know how to resonate around reducing arrays.
The biggest advantage is that the reducePath
function is side-effect free, meaning it does not need anything from the outside world and could be made static
. There are zero mutations in the function which help in debugging. Everything is coming in from the arguments and coming out by the return
.
Today we saw how to use PrimeNg with the TreeModule
. PrimeNg is a very complete set of components for Angular. Without it, implementing Tree
and DataTree
can be very challenging. We also saw how to use reduce
and recursion to build a tree structure out of a flat array following a more functional way. I do like reduce
a lot as I find it extremely useful. Hope you enjoyed reading this post as much as I enjoyed writing it! If you have any question, leave it here or hit me on Twitter @Kimserey_Lam. See you next time!