Tree structure in Angular with PrimeNg
Tree structure in Angular with PrimeNg
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.
This post will be composed by 3 parts:
1. Install PrimeNg
2. Mold data for Tree structure with reduce
3. Other use cases and benefits
1. Install PrimeNg
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.
"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.
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.
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:
- label: the label to be printed
- collapsedIon: the icon displayed when the TreeNode is collapsed
- expandedIcon: the icon displayed when the TreeNode is expanded, revealing its children
- children: children TreeNodes of the current TreeNode
- icon: the icon to be display regardless of any state
The result should be as followed:
2. Mold data for Tree structure with reduce
Tree structures are hard to construct. Especially for file paths, usually what we get is an array of path as followed:
[
'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:
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:
- taking each path one by one,
- dissecting the path by ‘/’
After that only three possibilities remain:
- if a file => add it as a node
- if a non existing folder => add the new folder as a node and recursively reduce the rest of the path
- if existing folder => recursively reduce
2.1 If a file => add it as a node
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.
reducePath = (nodes: TreeNode[], path: string) => {
const split = path.split('/');
// 2.1
if (split.length === 1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-file-o'
}
];
}
// will be removed
return [];
}
2.2 If a non existing folder => add the new folder as a node and recursively reduce the rest of the path
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.
reducePath = (nodes: TreeNode[], path: string) => {
const split = path.split('/');
// 2.1
if (split.length === 1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-file-o'
}
];
}
// 2.2
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 [];
}
2.3 If existing folder => recursively reduce
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.
reducePath = (nodes: TreeNode[], path: string) => {
const split = path.split('/');
// 2.1
if (split.length === 1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-file-o'
}
];
}
// 2.2
if (nodes.findIndex(n => n.label === split[0]) === -1) {
return [
...nodes,
{
label: split[0],
icon: 'fa-folder',
children: this.reducePath([], split.slice(1).join('/'))
}
];
}
// 2.3
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.
3. Other use cases and benefits
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
.
Conclusion
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!
Hi Kimserey,
ReplyDeleteI am new to PrimeNg.
I have very complex JSON that we need to display in tree view so we are using PrimeNG.
But I am not able to understand how to transform my complex JSON to Array of TreeNode.
Can you please help me.
Hi thanks for the comment, I can't really help if I don't see what is the "complex json" ;). Could you push it to a GitHub gist, maybe someone could take a look. Thanks!
ReplyDeleteThis may be too much to ask, but how would start with something like this:
ReplyDelete[
{
"id": 1,
"enspectOperationId": 22370,
"enspectCitationId": 18196,
"comments": "Something bad may have happened. Who knows?",
"observations": [
{
"id": 151,
"description": "Observation description.",
"citation": {
"id": 63,
"cfrCiteLang": "my cite langauge",
"citationDesc": "my citation description",
"issueDate": 1504137600000,
"assignment": {
"id": 122,
"userId": "denis",
"dateAssigned": 1504742400000,
"conveyanceType": 8,
"percentInterstate": 30,
"inspectionalResponsibilty": "someValue"
},
"citationShortDescription": "A short citations descriptions for the masses.",
"citationObservations": "Some really hairy long observation that no one (as in the GOT) will read.;)",
"dateCorrectionReceived": null,
"inspectionEndDate": 1504137600000,
"inspectionDate": 1504137600000
},
"correctiveActions": [
{
"id": 121,
"actionCode": 104,
"descriptionOfFirmCorrections": "my really wonderful corrections.",
"evidences": [],
"comments": [
{
"id": 3,
"comment": "My really cool comment."
}
]
},
{
"id": 126,
"actionCode": 104,
"descriptionOfFirmCorrections": "my really wonderful corrections.",
"evidences": [
{
"id": 124,
"comments": "my comments.",
"atchmtDets": [
{
"atchmntDetailId": 366,
"atchmnt": {
"atchmntId": 405,
"docUpldRqrd": "y",
"filName": "my file name",
"filPath": "bXkgZmlsZSBwYXRo\r\n",
"filSize": 9,
"filType": "really cool file type.",
"comments": "my really cool comments",
"numOfPages": 9,
"refCd": {
"refCdId": 2,
"refType": "EvidenceUpload",
"refValue": "Evidence for Corrective Action",
"description": "Evidence for Correct Action for a Citation",
"cFRCiteLanguage": null
}
}
}
]
}
],
"comments": [
{
"id": 6,
"comment": "My really cool comment."
}
]
}
],
"correctionStatus": {
"id": 1,
"status": "Not Corrected"
}
}
]
}
]
Hi Denis Putnam,
DeleteDid u got the solution ?? I have the same scenario..
Hi John, It's been a while since I posted this, but the project I am working on, I ended up using this: https://www.primefaces.org/primeng/#/datatable/rowexpansion
DeleteThis is a DataTable with Row Expansion. Pay attention to the . The ng-template sets up the part that expands, and the "let-car" lets you work with "car", so that you can reference the fields of "car", if that makes any sense.
I did not end up using a tree, per-se, but this version of p-dataTable is like a tree.
I hope that helps.
Thank you for sharing, saved my time :)
ReplyDeletehow to get data based on search in p-tree?Please give solution
ReplyDelete