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. @Component({
  6. selector: 'tree-view-menu',
  7. template: '<tree-view [menuList]="menuList"></tree-view>',
  8. styleUrls: ['./tree-view.css']
  9. })
  10. export class TreeViewComponent {
  11. public roleName: string;
  12. menuList: any;
  13. constructor(private _projectService: ProjectRoleService) {
  14. }
  15. ngOnInit() {
  16. this.roleName = "Admin";
  17. this._projectService.getMenuDetails(this.roleName).then((res:any) => {
  18. this.menuList = res;
  19. }, (error) => {
  20. swal("Failed to get Treeview menu details", error._body, "error");
  21. });
  22. }
  23. }

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

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. @NgModule({
  9. imports: [BrowserModule, HttpModule, AppRouteModule],
  10. declarations: [AppComponent, TreeViewComponent,TreeView
  11. ],
  12. bootstrap: [AppComponent],
  13. providers: [
  14. {
  15. provide: Http,
  16. useFactory: (xhrBackend: XHRBackend, requestOptions: RequestOptions, router: Router) => new HttpInterceptor(xhrBackend, requestOptions, router),
  17. deps: [XHRBackend, RequestOptions, Router]
  18. }
  19. ]
  20. })
  21. export class AppModule {
  22. }

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] [bit] NULL,
  12. [Path] [nvarchar](250) NULL,
  13. [DisplayOrder] [int] NULL,
  14. [ParentId] [int] NULL,
  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 = ON) ON [PRIMARY]
  27. ) ON [PRIMARY]
  28. GO
  29. SET ANSI_PADDING ON
  30. GO
  31. /****** Object: Table [dbo].[MenuRoles] ******/
  32. SET ANSI_NULLS ON
  33. GO
  34. SET QUOTED_IDENTIFIER ON
  35. GO
  36. SET ANSI_PADDING ON
  37. GO
  38. CREATE TABLE [dbo].[MenuRoles](
  39. [MenuRoleId] [int] IDENTITY(1,1) NOT NULL,
  40. [MenuId] [int] NOT NULL,
  41. [RoleId] [int] NULL,
  42. [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_MenuRoles_CreatedUser] DEFAULT (suser_sname()),
  43. [ModifiedUser] [varchar](100) NULL,
  44. [CreatedDate] [datetime] NULL CONSTRAINT [DF_MenuRoles_CreatedDate] DEFAULT (getdate()),
  45. [ModifiedDate] [datetime] NULL,
  46. [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_MenuRoles_SystemInfo] DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),
  47. CONSTRAINT [PK_MenuRoles] PRIMARY KEY CLUSTERED
  48. (
  49. [MenuRoleId] ASC
  50. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
  51. ) ON [PRIMARY]
  52. GO
  53. SET ANSI_PADDING ON
  54. GO
  55. /****** Object: Table [dbo].[Roles] ******/
  56. SET ANSI_NULLS ON
  57. GO
  58. SET QUOTED_IDENTIFIER ON
  59. GO
  60. SET ANSI_PADDING ON
  61. GO
  62. CREATE TABLE [dbo].[Roles](
  63. [RoleId] [int] IDENTITY(1,1) NOT NULL,
  64. [RoleName] [nvarchar](256) NULL,
  65. [RoleDescription] [nvarchar](256) NULL,
  66. [IsActive] [bit] NULL,
  67. [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_Roles_CreatedUser] DEFAULT (suser_sname()),
  68. [ModifiedUser] [varchar](100) NULL,
  69. [CreatedDate] [datetime] NULL CONSTRAINT [DF_Roles_CreatedDate] DEFAULT (getdate()),
  70. [ModifiedDate] [datetime] NULL,
  71. [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_Roles_SystemInfo] DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),
  72. [DepartmentId] [int] NULL,
  73. [KeyResponsibilities] [nvarchar](max) NULL,
  74. [EducationQualification] [nvarchar](max) NULL,
  75. CONSTRAINT [PK_RoleId] PRIMARY KEY CLUSTERED
  76. (
  77. [RoleId] ASC
  78. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
  79. ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
  80. GO
  81. SET ANSI_PADDING ON
  82. 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. namespace AP.API
  8. {
  9. public class Menu
  10. {
  11. public IEnumerable<MenuData> GetMenuDetails(string roleName)
  12. {
  13. try
  14. {
  15. return GetMenuDetailsByRole(roleName);
  16. }
  17. catch
  18. {
  19. throw;
  20. }
  21. return null;
  22. }
  23. private IEnumerable<MenuData> GetMenuDetailsByRole(string roleName)
  24. {
  25. IEnumerable<MenuData> menuData;
  26. IEnumerable<MenuData> _menuParentNodesData;
  27. using (APEntities hrmsEntities = new APEntities())
  28. {
  29. var query = (from menu in hrmsEntities.Menus
  30. join menuRoles in hrmsEntities.MenuRoles on menu.MenuId equals menuRoles.MenuId
  31. join roles in hrmsEntities.Roles on menuRoles.RoleId equals roles.RoleId
  32. where menu.IsActive == true && roles.RoleName == roleName
  33. orderby menu.DisplayOrder ascending
  34. select new MenuData
  35. {
  36. MenuId = menu.MenuId,
  37. Title = menu.Title,
  38. IsActive = menu.IsActive,
  39. Path = menu.Path,
  40. DisplayOrder = menu.DisplayOrder,
  41. ParentId = menu.ParentId,
  42. Parameter = menu.Parameter,
  43. NodeId = menu.NodeId,
  44. Style = menu.Style
  45. });
  46. menuData = query.ToList();
  47. if (menuData != null && menuData.Count() > 1)
  48. {
  49. _menuParentNodesData = menuData.Where(menu => menu.ParentId == 0);
  50. foreach (var menuItem in _menuParentNodesData)
  51. {
  52. buildTreeviewMenu(menuItem, menuData);
  53. }
  54. }
  55. else
  56. _menuParentNodesData = new MenuData[] {};
  57. }
  58. return _menuParentNodesData;
  59. }
  60. private void buildTreeviewMenu(MenuData menuItem, IEnumerable<MenuData> menudata)
  61. {
  62. IEnumerable<MenuData> _menuItems;
  63. _menuItems = menudata.Where(menu => menu.ParentId == menuItem.MenuId);
  64. if (_menuItems != null && _menuItems.Count() > 0)
  65. {
  66. foreach (var item in _menuItems)
  67. {
  68. menuItem.Categories.Add(item);
  69. buildTreeviewMenu(item, menudata);
  70. }
  71. }
  72. }
  73. }
  74. }

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. public MenuData()
  14. {
  15. Categories = new List<MenuData>();
  16. MenuRoles = new List<RoleData>();
  17. }
  18. }

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