Recursive Tree-View Using Angular 2 And TypeScript Until nth Level Depth

In this article, I will show you how to create a simple Nth level recursive tree-view using Angular 2 and TypeScript. Here, I am using TypeScript, which is not mandatory. You can use your own supported scripting language, which can be either JavaScript or any other.

I will not go into much detail of each and every piece of code, as I am assuming you already know Angular 2 coding style.

I will start by creating a wrapper component that Bootstraps the Application and includes the actual Tree-View component on the page. This sample Tree-View renders a recursive Tree-View structure with the parent nodes, child nodes, grandchild nodes, grand grand child nodes and so on in a parent child relationship fashion.

Step 1

tree-view.component.ts

  1. import { Component } from '@angular/core';  
  2. import { TreeView } from './tree-view.directive';  
  3. import { ProjectRoleService } from '../../services/project-role.service';  
  4. import swal from 'sweetalert2';  
  5.   
  6. @Component({  
  7.     selector: 'tree-view-menu',      
  8.     template: '<tree-view [menuList]="menuList"></tree-view>',  
  9.     styleUrls: ['./tree-view.css']      
  10. })  
  11. export class TreeViewComponent {  
  12.     public roleName: string;  
  13.     menuList: any;  
  14.     constructor(private _projectService: ProjectRoleService) {          
  15.     }  
  16.     ngOnInit() {  
  17.          this.roleName = "Admin";  
  18.          this._projectService.getMenuDetails(this.roleName).then((res:any) => {  
  19.          this.menuList = res;  
  20.          }, (error) => {  
  21.              swal("Failed to get Treeview menu details", error._body, "error");  
  22.         });  
  23.      }  
  24. }  

In the code given above, getMenuDetails is a Service method, once the Service method executes successfully. Subsequently, it will give you the response in JSON format, as shown below. We are calling this Service method inside one of the Angular 2 lifecycle event, which is ngOnInit.

  1. [  
  2.             {  
  3.                 title: 'Parent 1',  
  4.                 routerLink: '/ap/dashboard',  
  5.                 style: 'fa fa-home',  
  6.                 nodeId: 'liDashboard',  
  7.                 param: '',  
  8.                 categories: []  
  9.             },  
  10.             {  
  11.                 title: 'Parent 2',  
  12.                 routerLink: '',  
  13.                 style: 'fa fa-users',  
  14.                 nodeId: 'liAssociates',  
  15.                 param: '',  
  16.                 categories: [  
  17.                     {  
  18.                         title: 'Child 1',  
  19.                         style: 'fa fa-users',  
  20.                         nodeId: 'liProspectiveAssociate',  
  21.                         param: '',  
  22.                         routerLink: '/ap/associates/view',  
  23.                         categories: [  
  24.                                         {  
  25.                                             title: 'Grand Child 1',  
  26.                                             style: 'fa fa-users',  
  27.                                             nodeId: 'A',  
  28.                                             param: '',  
  29.                                             routerLink: '/ap/associates/view',  
  30.                                             categories: [  
  31.                                                             {  
  32.                                                                 title: 'Grand Grand Child 1',  
  33.                                                                 style: 'fa fa-user-plus',  
  34.                                                                 nodeId: 'D',  
  35.                                                                 param: '',  
  36.                                                                 routerLink: '/ap/reports/resourcereport',  
  37.                                                                 categories: []  
  38.                                                             },  
  39.                                                             {  
  40.                                                                 title: 'Grand Grand Child 2',  
  41.                                                                 style: 'fa fa-user-plus',  
  42.                                                                 nodeId: 'E',  
  43.                                                                 param: '',  
  44.                                                                 routerLink: '/ap/reports/financereport',  
  45.                                                                 categories: []  
  46.                                                             },  
  47.                                                             {  
  48.                                                                 title: 'Grand Grand Child 3',  
  49.                                                                 style: 'fa fa-pencil-square',  
  50.                                                                 nodeId: 'F',  
  51.                                                                 param: '',  
  52.                                                                 routerLink: '/ap/reports/importRMGreport',  
  53.                                                                 categories: []  
  54.                                                             }  
  55.                                                     ]  
  56.                                         },  
  57.                                         {  
  58.                                             title: 'Grand Child 2',  
  59.                                             style: 'fa fa-pencil-square',  
  60.                                             nodeId: 'B',  
  61.                                             param: '',  
  62.                                             routerLink: '/ap/associates/prospective-associates',  
  63.                                             categories: []  
  64.                                         },  
  65.                                         {  
  66.                                             title: 'Grand Child 3',  
  67.                                             style: 'fa fa-users',  
  68.                                             nodeId: 'C',  
  69.                                             param: '',  
  70.                                             routerLink: '/ap/associates/list',  
  71.                                             categories: []  
  72.                                         }  
  73.                                 ]  
  74.                     },  
  75.                     {  
  76.                         title: 'Child 2',  
  77.                         style: 'fa fa-pencil-square',  
  78.                         nodeId: 'liAssociateJoining',  
  79.                         param: '',  
  80.                         routerLink: '/ap/associates/prospective-associates',  
  81.                         categories: []  
  82.                     },  
  83.                     {  
  84.                         title: 'Child 3',  
  85.                         style: 'fa fa-users',  
  86.                         nodeId: 'liAssociatesChild',  
  87.                         param: '',  
  88.                         routerLink: '/ap/associates/list',  
  89.                         categories: []  
  90.                     }  
  91.                 ]  
  92.             },  
  93.             {  
  94.                 title: 'Parent 3',  
  95.                 routerLink: '',  
  96.                 style: 'fa fa-users',  
  97.                 nodeId: 'liTalentManagement',  
  98.                 param: '',  
  99.                 categories: []  
  100.             },  
  101.             {  
  102.                 title: 'Parent 4',  
  103.                 routerLink: '',  
  104.                 style: 'fa fa-users',  
  105.                 nodeId: 'liTeamManagement',  
  106.                 param: '',  
  107.                 categories: []  
  108.             },  
  109.             {  
  110.                 title: 'Parent 5',  
  111.                 routerLink: '',  
  112.                 style: 'fa fa-users',  
  113.                 nodeId: 'liPerformanceManagement',  
  114.                 param: '',  
  115.                 categories: []  
  116.             },  
  117.             {  
  118.                 title: 'Parent 6',  
  119.                 routerLink: '',  
  120.                 style: 'fa fa-street-view',  
  121.                 nodeId: 'liAdmin',  
  122.                 param: '',  
  123.                 categories: [ ]  
  124.             },  
  125.             {  
  126.                 title: 'Parent 7',  
  127.                 routerLink: '',  
  128.                 style: 'fa fa-users',  
  129.                 nodeId: 'liReports',  
  130.                 param: '',  
  131.                 categories: []  
  132.             }  
  133.         ]  

Step 2

This is the Service component, where we are dealing with HTTP verbs. The Services are running at http://localhost:8080/api.services. Once the getMenuDetails Service method executes, it will return response in hierarchical data structure format as a parent child relationship in JSON format. The actual logic to generate the hierarchical data is written, using C, which you will find while going forward.

project-role.service.ts

  1. import { Injectable, Inject } from '@angular/core';  
  2. import { Observable } from 'rxjs/Observable';  
  3. import { Http } from '@angular/http';  
  4. import 'rxjs/Rx';  
  5.   
  6. @Injectable()  
  7. export class ProjectRoleService {  
  8.     constructor(private _http: Http) {  
  9.         this._serverURL = "http://localhost:8080/api.services";  
  10.     }      
  11. getMenuDetails(roleName: string) {  
  12.             let _url = this._serverURL + "/Menu/GetMenuDetails?roleName=" + roleName;  
  13.   
  14.             return new Promise((resolve, reject) => {  
  15.                 this._http.get(_url)  
  16.                     .map(res =>res.json())  
  17.                     .catch((error: any) => {  
  18. console.error(error);  
  19.                         reject(error);  
  20.                         return Observable.throw(error.json().error || 'Server error');  
  21.                     })  
  22.                     .subscribe((data) => {  
  23.                         resolve(data);  
  24.                     });  
  25.             });  
  26.         }  
  27. }  

Step 3

The piece of code given below is required to generate the recursive Tree-View component. This component file contains the actual template URL with selector and munuList input property.

tree-view.directory.ts

  1. import {Component, Input} from '@angular/core';  
  2. @Component({  
  3.     selector: 'tree-view',      
  4.     templateUrl: './tree-view.html',  
  5.     styleUrls: ['./tree-view.css']      
  6. })  
  7. export class TreeView {  
  8.     @Input() menuList: any;  
  9. }  

Step 4

Tree-View component is included in the main component (tree-view.component.ts) as <tree-view-menu></tree-view-menu>, but notice in this HTML for the Tree-View, as there is a self reference. This is important, since it's how I am rendering the nodes recursively.

tree-view.html

  1. <ul class="sidebar-menu">  
  2.     <li class="treeview" *ngFor="let parentNode of menuList">  
  3.         <a *ngIf="parentNode.Path == ''" href="" id="parentNode.NodeId">  
  4.             <i [ngClass]="parentNode.Style"></i><span> {{ parentNode.Title }}</span>  
  5.             <i *ngIf="parentNode.Categories.length > 0" class="fa fa-angle-left pull-right"></i>  
  6.         </a>  
  7.         <a *ngIf="parentNode.Path != ''" [routerLink]="[parentNode.Path]"  
  8.             id="parentNode.NodeId">  
  9.             <i [ngClass]="parentNode.Style"></i><span> {{ parentNode.Title }}</span>  
  10.             <i *ngIf="parentNode.Categories.length > 0" class="fa fa-angle-left pull-right"></i>  
  11.         </a>  
  12.         <ul class="treeview-menu">  
  13.             <li *ngFor="let childNode of parentNode.Categories">  
  14.                 <a [routerLinkActive]="['active']" [routerLink]="[childNode.Path]"  
  15.                     id="childNode.NodeId">  
  16.                     <i [ngClass]="childNode.Style"></i><span>{{childNode.Title}}</span>  
  17.                     <i *ngIf="childNode.Categories.length > 0" class="fa fa-angle-left pull-right"></i>  
  18.                 </a>  
  19.                 <div *ngIf="childNode.Categories.length > 0" class="treeview-menu">  
  20.                     <tree-view [menuList]="childNode.Categories"></tree-view>  
  21.                 </div>  
  22.             </li>  
  23.         </ul>  
  24.     </li>  
  25. </ul>  

Step 5

The actual TreeView components (TreeView and TreeViewComponent), which we are declaring in module.ts file is because usually this is the entry point for an Angular 2 Application.

app.module.ts

  1. import { NgModule, ErrorHandler, Injector } from '@angular/core';  
  2. import { BrowserModule } from '@angular/platform-browser';  
  3. import { Router, ActivatedRoute } from '@angular/router';  
  4. import { AppComponent } from './app.component';  
  5. import { Observable } from 'rxjs/Observable';  
  6. import {TreeView} from './shared/tree-view-menu/tree-view.directive';  
  7. import {TreeViewComponent} from './shared/tree-view-menu/tree-view.component';  
  8.   
  9. @NgModule({  
  10.     imports: [BrowserModule, HttpModule, AppRouteModule],  
  11.     declarations: [AppComponent, TreeViewComponent,TreeView                
  12.     ],    
  13.     bootstrap: [AppComponent],  
  14.     providers: [  
  15.         {  
  16.             provide: Http,  
  17.             useFactory: (xhrBackend: XHRBackend, requestOptions: RequestOptions, router: Router) => new HttpInterceptor(xhrBackend, requestOptions, router),  
  18.             deps: [XHRBackend, RequestOptions, Router]  
  19.         }  
  20.     ]  
  21. })  
  22. export class AppModule {  
  23. }  

Step 6

The actual "<tree-view-menu>" selector is the one, which we are injecting in the startup index page or in the layout page.

_layout.component.html

  1. <div class="wrapper">  
  2.     <header class="main-header">  
  3.         <nav class="navbarnavbar-static-top" role="navigation"> </nav>  
  4.     </header>  
  5.     <aside class="main-sidebar">  
  6.         <section class="sidebar">  
  7.             <tree-view-menu>Loading...</tree-view-menu>  
  8.         </section>  
  9.     </aside>  
  10.     <div class="content-wrapper">  
  11.         <section class="content">  
  12.             <router-outlet></router-outlet>  
  13.         </section>  
  14.     </div>  
  15.     <footer class="main-footer"> </footer>  
  16. </div>  

Step 7

Database tables script

  1. /****** Object:  Table [dbo].[Menu] ******/  
  2. SET ANSI_NULLS ON  
  3. GO  
  4. SET QUOTED_IDENTIFIER ON  
  5. GO  
  6. SET ANSI_PADDING ON  
  7. GO  
  8. CREATE TABLE [dbo].[Menu](  
  9.     [MenuId] [int] IDENTITY(1,1) NOT NULL,  
  10.     [Title] [nvarchar](50) NOT NULL,  
  11.     [IsActive] [bitNULL,  
  12.     [Path] [nvarchar](250) NULL,  
  13.     [DisplayOrder] [intNULL,  
  14.     [ParentId] [intNULL,  
  15.     [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_Menu_CreatedUser]  DEFAULT (suser_sname()),  
  16.     [ModifiedUser] [varchar](100) NULL,  
  17.     [CreatedDate] [datetime] NULL CONSTRAINT [DF_Menu_CreatedDate]  DEFAULT (getdate()),  
  18.     [ModifiedDate] [datetime] NULL,  
  19.     [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_Menu_SystemInfo]  DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),  
  20.     [Parameter] [nvarchar](50) NULL,  
  21.     [NodeId] [nvarchar](50) NULL,  
  22.     [Style] [nvarchar](50) NULL,  
  23.  CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED   
  24. (  
  25.     [MenuId] ASC  
  26. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ONON [PRIMARY]  
  27. ON [PRIMARY]  
  28.   
  29. GO  
  30. SET ANSI_PADDING ON  
  31. GO  
  32. /****** Object:  Table [dbo].[MenuRoles]   ******/  
  33. SET ANSI_NULLS ON  
  34. GO  
  35. SET QUOTED_IDENTIFIER ON  
  36. GO  
  37. SET ANSI_PADDING ON  
  38. GO  
  39. CREATE TABLE [dbo].[MenuRoles](  
  40.     [MenuRoleId] [int] IDENTITY(1,1) NOT NULL,  
  41.     [MenuId] [intNOT NULL,  
  42.     [RoleId] [intNULL,  
  43.     [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_MenuRoles_CreatedUser]  DEFAULT (suser_sname()),  
  44.     [ModifiedUser] [varchar](100) NULL,  
  45.     [CreatedDate] [datetime] NULL CONSTRAINT [DF_MenuRoles_CreatedDate]  DEFAULT (getdate()),  
  46.     [ModifiedDate] [datetime] NULL,  
  47.     [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_MenuRoles_SystemInfo]  DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),  
  48.  CONSTRAINT [PK_MenuRoles] PRIMARY KEY CLUSTERED   
  49. (  
  50.     [MenuRoleId] ASC  
  51. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ONON [PRIMARY]  
  52. ON [PRIMARY]  
  53.   
  54. GO  
  55. SET ANSI_PADDING ON  
  56. GO  
  57. /****** Object:  Table [dbo].[Roles]    ******/  
  58. SET ANSI_NULLS ON  
  59. GO  
  60. SET QUOTED_IDENTIFIER ON  
  61. GO  
  62. SET ANSI_PADDING ON  
  63. GO  
  64. CREATE TABLE [dbo].[Roles](  
  65.     [RoleId] [int] IDENTITY(1,1) NOT NULL,  
  66.     [RoleName] [nvarchar](256) NULL,  
  67.     [RoleDescription] [nvarchar](256) NULL,  
  68.     [IsActive] [bitNULL,  
  69.     [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_Roles_CreatedUser]  DEFAULT (suser_sname()),  
  70.     [ModifiedUser] [varchar](100) NULL,  
  71.     [CreatedDate] [datetime] NULL CONSTRAINT [DF_Roles_CreatedDate]  DEFAULT (getdate()),  
  72.     [ModifiedDate] [datetime] NULL,  
  73.     [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_Roles_SystemInfo]  DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),  
  74.     [DepartmentId] [intNULL,  
  75.     [KeyResponsibilities] [nvarchar](maxNULL,  
  76.     [EducationQualification] [nvarchar](maxNULL,  
  77.  CONSTRAINT [PK_RoleId] PRIMARY KEY CLUSTERED   
  78. (  
  79.     [RoleId] ASC  
  80. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ONON [PRIMARY]  
  81. ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]  
  82.   
  83. GO  
  84. SET ANSI_PADDING ON  
  85. GO  

Step 8

Business logic

C# code starts from here. The actual logic to generate hierarchical data structure until Nth level is written using C#. Here, I have used EntityFramework database first approach to get Tree-View data source in JSON format till nth level depth, using recursive mechanism.

Menu.cs

  1. using System;  
  2. using System.Linq;  
  3. using System.Collections.Generic;  
  4. using System.Data.Entity;  
  5. using EntityFramework.MappingAPI;  
  6. using Newtonsoft.Json;  
  7.   
  8. namespace AP.API  
  9. {  
  10.     public class Menu  
  11.     {  
  12.         public IEnumerable<MenuData> GetMenuDetails(string roleName)  
  13.         {  
  14.             try  
  15.             {  
  16.                 return GetMenuDetailsByRole(roleName);  
  17.             }  
  18.             catch  
  19.             {  
  20.                 throw;  
  21.             }  
  22.             return null;  
  23.         }  
  24.   
  25.         private IEnumerable<MenuData> GetMenuDetailsByRole(string roleName)  
  26.         {  
  27.             IEnumerable<MenuData> menuData;  
  28.             IEnumerable<MenuData> _menuParentNodesData;  
  29.   
  30.             using (APEntities hrmsEntities = new APEntities())  
  31.             {  
  32.                 var query = (from menu in hrmsEntities.Menus  
  33.                              join menuRoles in hrmsEntities.MenuRoles on menu.MenuId equals menuRoles.MenuId  
  34.                              join roles in hrmsEntities.Roles on menuRoles.RoleId equals roles.RoleId  
  35.                              where menu.IsActive == true && roles.RoleName == roleName  
  36.                              orderby menu.DisplayOrder ascending  
  37.                              select new MenuData  
  38.                              {  
  39.                                  MenuId = menu.MenuId,  
  40.                                  Title = menu.Title,  
  41.                                  IsActive = menu.IsActive,  
  42.                                  Path = menu.Path,  
  43.                                  DisplayOrder = menu.DisplayOrder,  
  44.                                  ParentId = menu.ParentId,  
  45.                                  Parameter = menu.Parameter,  
  46.                                  NodeId = menu.NodeId,  
  47.                                  Style = menu.Style  
  48.                              });  
  49.                 menuData = query.ToList();  
  50.   
  51.                 if (menuData != null && menuData.Count() > 1)  
  52.                 {  
  53.                     _menuParentNodesData = menuData.Where(menu => menu.ParentId == 0);  
  54.   
  55.                     foreach (var menuItem in _menuParentNodesData)  
  56.                     {  
  57.                         buildTreeviewMenu(menuItem, menuData);  
  58.                     }  
  59.                 }  
  60.                 else  
  61.                     _menuParentNodesData = new MenuData[] {};  
  62.             }  
  63.             return _menuParentNodesData;  
  64.         }  
  65.   
  66.         private void buildTreeviewMenu(MenuData menuItem, IEnumerable<MenuData> menudata)  
  67.         {  
  68.             IEnumerable<MenuData> _menuItems;  
  69.   
  70.             _menuItems = menudata.Where(menu => menu.ParentId == menuItem.MenuId);  
  71.   
  72.             if (_menuItems != null && _menuItems.Count() > 0)  
  73.             {  
  74.                 foreach (var item in _menuItems)  
  75.                 {  
  76.                     menuItem.Categories.Add(item);  
  77.                     buildTreeviewMenu(item, menudata);  
  78.                 }  
  79.             }  
  80.         }  
  81.     }  

Step 9

This is a DTO class by which the actual data communicates.

MenuData.cs

  1. public class MenuData:BaseEntity  
  2.     {  
  3.         public int MenuId { get; set; }  
  4.         public string Title { get; set; }  
  5.         public string Path { get; set; }  
  6.         public int? ParentId { get; set; }  
  7.         public int? DisplayOrder { get; set; }  
  8.         public string Parameter { get; set; }  
  9.         public string NodeId { get; set; }  
  10.         public string Style { get; set; }  
  11.         public List<MenuData> Categories { get; set; }  
  12.         public IEnumerable<RoleData> MenuRoles { get; set; }  
  13.    
  14.         public  MenuData()  
  15.         {  
  16.             Categories = new List<MenuData>();  
  17.             MenuRoles = new List<RoleData>();  
  18.         }  
  19.     }  

Step 10

MenuController.cs

Here, I have used Web API controller as a Service class.

  1. using System;  
  2. using AP.API;  
  3. using AP.DomainEntities;  
  4. using System.Collections.Generic;  
  5. using Newtonsoft.Json;  
  6.   
  7. namespace AP.Services.Controllers  
  8. {  
  9.     public class MenuController : ApiController  
  10.     {  
  11.         /// <summary>  
  12.         /// Get Menu Details by logged in user role  
  13.         /// </summary>  
  14.         /// <param name="email ID"></param>  
  15.         /// <returns></returns>  
  16.         [HttpGet]  
  17.         public HttpResponseMessage GetMenuDetails(string roleName)  
  18.         {  
  19.             HttpResponseMessage httpResponseMessage = null;  
  20.   
  21.             try  
  22.             {  
  23.                 httpResponseMessage = Request.CreateResponse(new Menu().GetMenuDetails(roleName));  
  24.             }  
  25.             catch (Exception ex)  
  26.             {  
  27.                 Throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)  
  28.                 {  
  29.                     Content = new StringContent(ex.Message),  
  30.                     ReasonPhrase = "Warning"  
  31.                 });  
  32.             }  
  33.             return httpResponseMessage;  
  34.         }  
  35.     }  
  36. }  
Step 10

To get this output, I have used Admin LTE CSS, which was my requirement. That is not mandatory and you can go with either simple CSS classes or you can go with Bootstrap CSS or you can go with the material design, which is based on your interest and requirement.

Final output

Output

Happy coding.