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
- import { Component } from '@angular/core';
- import { TreeView } from './tree-view.directive';
- import { ProjectRoleService } from '../../services/project-role.service';
- import swal from 'sweetalert2';
-
- @Component({
- selector: 'tree-view-menu',
- template: '<tree-view [menuList]="menuList"></tree-view>',
- styleUrls: ['./tree-view.css']
- })
- export class TreeViewComponent {
- public roleName: string;
- menuList: any;
- constructor(private _projectService: ProjectRoleService) {
- }
- ngOnInit() {
- this.roleName = "Admin";
- this._projectService.getMenuDetails(this.roleName).then((res:any) => {
- this.menuList = res;
- }, (error) => {
- swal("Failed to get Treeview menu details", error._body, "error");
- });
- }
- }
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.
- [
- {
- title: 'Parent 1',
- routerLink: '/ap/dashboard',
- style: 'fa fa-home',
- nodeId: 'liDashboard',
- param: '',
- categories: []
- },
- {
- title: 'Parent 2',
- routerLink: '',
- style: 'fa fa-users',
- nodeId: 'liAssociates',
- param: '',
- categories: [
- {
- title: 'Child 1',
- style: 'fa fa-users',
- nodeId: 'liProspectiveAssociate',
- param: '',
- routerLink: '/ap/associates/view',
- categories: [
- {
- title: 'Grand Child 1',
- style: 'fa fa-users',
- nodeId: 'A',
- param: '',
- routerLink: '/ap/associates/view',
- categories: [
- {
- title: 'Grand Grand Child 1',
- style: 'fa fa-user-plus',
- nodeId: 'D',
- param: '',
- routerLink: '/ap/reports/resourcereport',
- categories: []
- },
- {
- title: 'Grand Grand Child 2',
- style: 'fa fa-user-plus',
- nodeId: 'E',
- param: '',
- routerLink: '/ap/reports/financereport',
- categories: []
- },
- {
- title: 'Grand Grand Child 3',
- style: 'fa fa-pencil-square',
- nodeId: 'F',
- param: '',
- routerLink: '/ap/reports/importRMGreport',
- categories: []
- }
- ]
- },
- {
- title: 'Grand Child 2',
- style: 'fa fa-pencil-square',
- nodeId: 'B',
- param: '',
- routerLink: '/ap/associates/prospective-associates',
- categories: []
- },
- {
- title: 'Grand Child 3',
- style: 'fa fa-users',
- nodeId: 'C',
- param: '',
- routerLink: '/ap/associates/list',
- categories: []
- }
- ]
- },
- {
- title: 'Child 2',
- style: 'fa fa-pencil-square',
- nodeId: 'liAssociateJoining',
- param: '',
- routerLink: '/ap/associates/prospective-associates',
- categories: []
- },
- {
- title: 'Child 3',
- style: 'fa fa-users',
- nodeId: 'liAssociatesChild',
- param: '',
- routerLink: '/ap/associates/list',
- categories: []
- }
- ]
- },
- {
- title: 'Parent 3',
- routerLink: '',
- style: 'fa fa-users',
- nodeId: 'liTalentManagement',
- param: '',
- categories: []
- },
- {
- title: 'Parent 4',
- routerLink: '',
- style: 'fa fa-users',
- nodeId: 'liTeamManagement',
- param: '',
- categories: []
- },
- {
- title: 'Parent 5',
- routerLink: '',
- style: 'fa fa-users',
- nodeId: 'liPerformanceManagement',
- param: '',
- categories: []
- },
- {
- title: 'Parent 6',
- routerLink: '',
- style: 'fa fa-street-view',
- nodeId: 'liAdmin',
- param: '',
- categories: [ ]
- },
- {
- title: 'Parent 7',
- routerLink: '',
- style: 'fa fa-users',
- nodeId: 'liReports',
- param: '',
- categories: []
- }
- ]
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
- import { Injectable, Inject } from '@angular/core';
- import { Observable } from 'rxjs/Observable';
- import { Http } from '@angular/http';
- import 'rxjs/Rx';
-
- @Injectable()
- export class ProjectRoleService {
- constructor(private _http: Http) {
- this._serverURL = "http://localhost:8080/api.services";
- }
- getMenuDetails(roleName: string) {
- let _url = this._serverURL + "/Menu/GetMenuDetails?roleName=" + roleName;
-
- return new Promise((resolve, reject) => {
- this._http.get(_url)
- .map(res =>res.json())
- .catch((error: any) => {
- console.error(error);
- reject(error);
- return Observable.throw(error.json().error || 'Server error');
- })
- .subscribe((data) => {
- resolve(data);
- });
- });
- }
- }
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
- import {Component, Input} from '@angular/core';
- @Component({
- selector: 'tree-view',
- templateUrl: './tree-view.html',
- styleUrls: ['./tree-view.css']
- })
- export class TreeView {
- @Input() menuList: any;
- }
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
- <ul class="sidebar-menu">
- <li class="treeview" *ngFor="let parentNode of menuList">
- <a *ngIf="parentNode.Path == ''" href="" id="parentNode.NodeId">
- <i [ngClass]="parentNode.Style"></i><span> {{ parentNode.Title }}</span>
- <i *ngIf="parentNode.Categories.length > 0" class="fa fa-angle-left pull-right"></i>
- </a>
- <a *ngIf="parentNode.Path != ''" [routerLink]="[parentNode.Path]"
- id="parentNode.NodeId">
- <i [ngClass]="parentNode.Style"></i><span> {{ parentNode.Title }}</span>
- <i *ngIf="parentNode.Categories.length > 0" class="fa fa-angle-left pull-right"></i>
- </a>
- <ul class="treeview-menu">
- <li *ngFor="let childNode of parentNode.Categories">
- <a [routerLinkActive]="['active']" [routerLink]="[childNode.Path]"
- id="childNode.NodeId">
- <i [ngClass]="childNode.Style"></i><span>{{childNode.Title}}</span>
- <i *ngIf="childNode.Categories.length > 0" class="fa fa-angle-left pull-right"></i>
- </a>
- <div *ngIf="childNode.Categories.length > 0" class="treeview-menu">
- <tree-view [menuList]="childNode.Categories"></tree-view>
- </div>
- </li>
- </ul>
- </li>
- </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
- import { NgModule, ErrorHandler, Injector } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
- import { Router, ActivatedRoute } from '@angular/router';
- import { AppComponent } from './app.component';
- import { Observable } from 'rxjs/Observable';
- import {TreeView} from './shared/tree-view-menu/tree-view.directive';
- import {TreeViewComponent} from './shared/tree-view-menu/tree-view.component';
-
- @NgModule({
- imports: [BrowserModule, HttpModule, AppRouteModule],
- declarations: [AppComponent, TreeViewComponent,TreeView
- ],
- bootstrap: [AppComponent],
- providers: [
- {
- provide: Http,
- useFactory: (xhrBackend: XHRBackend, requestOptions: RequestOptions, router: Router) => new HttpInterceptor(xhrBackend, requestOptions, router),
- deps: [XHRBackend, RequestOptions, Router]
- }
- ]
- })
- export class AppModule {
- }
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
- <div class="wrapper">
- <header class="main-header">
- <nav class="navbarnavbar-static-top" role="navigation"> </nav>
- </header>
- <aside class="main-sidebar">
- <section class="sidebar">
- <tree-view-menu>Loading...</tree-view-menu>
- </section>
- </aside>
- <div class="content-wrapper">
- <section class="content">
- <router-outlet></router-outlet>
- </section>
- </div>
- <footer class="main-footer"> </footer>
- </div>
Step 7
Database tables script
- /****** Object: Table [dbo].[Menu] ******/
- SET ANSI_NULLS ON
- GO
- SET QUOTED_IDENTIFIER ON
- GO
- SET ANSI_PADDING ON
- GO
- CREATE TABLE [dbo].[Menu](
- [MenuId] [int] IDENTITY(1,1) NOT NULL,
- [Title] [nvarchar](50) NOT NULL,
- [IsActive] [bit] NULL,
- [Path] [nvarchar](250) NULL,
- [DisplayOrder] [int] NULL,
- [ParentId] [int] NULL,
- [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_Menu_CreatedUser] DEFAULT (suser_sname()),
- [ModifiedUser] [varchar](100) NULL,
- [CreatedDate] [datetime] NULL CONSTRAINT [DF_Menu_CreatedDate] DEFAULT (getdate()),
- [ModifiedDate] [datetime] NULL,
- [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_Menu_SystemInfo] DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),
- [Parameter] [nvarchar](50) NULL,
- [NodeId] [nvarchar](50) NULL,
- [Style] [nvarchar](50) NULL,
- CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED
- (
- [MenuId] ASC
- )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
- ) ON [PRIMARY]
-
- GO
- SET ANSI_PADDING ON
- GO
- /****** Object: Table [dbo].[MenuRoles] ******/
- SET ANSI_NULLS ON
- GO
- SET QUOTED_IDENTIFIER ON
- GO
- SET ANSI_PADDING ON
- GO
- CREATE TABLE [dbo].[MenuRoles](
- [MenuRoleId] [int] IDENTITY(1,1) NOT NULL,
- [MenuId] [int] NOT NULL,
- [RoleId] [int] NULL,
- [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_MenuRoles_CreatedUser] DEFAULT (suser_sname()),
- [ModifiedUser] [varchar](100) NULL,
- [CreatedDate] [datetime] NULL CONSTRAINT [DF_MenuRoles_CreatedDate] DEFAULT (getdate()),
- [ModifiedDate] [datetime] NULL,
- [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_MenuRoles_SystemInfo] DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),
- CONSTRAINT [PK_MenuRoles] PRIMARY KEY CLUSTERED
- (
- [MenuRoleId] ASC
- )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
- ) ON [PRIMARY]
-
- GO
- SET ANSI_PADDING ON
- GO
- /****** Object: Table [dbo].[Roles] ******/
- SET ANSI_NULLS ON
- GO
- SET QUOTED_IDENTIFIER ON
- GO
- SET ANSI_PADDING ON
- GO
- CREATE TABLE [dbo].[Roles](
- [RoleId] [int] IDENTITY(1,1) NOT NULL,
- [RoleName] [nvarchar](256) NULL,
- [RoleDescription] [nvarchar](256) NULL,
- [IsActive] [bit] NULL,
- [CreatedUser] [varchar](100) NULL CONSTRAINT [DF_Roles_CreatedUser] DEFAULT (suser_sname()),
- [ModifiedUser] [varchar](100) NULL,
- [CreatedDate] [datetime] NULL CONSTRAINT [DF_Roles_CreatedDate] DEFAULT (getdate()),
- [ModifiedDate] [datetime] NULL,
- [SystemInfo] [varchar](50) NULL CONSTRAINT [DF_Roles_SystemInfo] DEFAULT (CONVERT([char](15),connectionproperty('client_net_address'))),
- [DepartmentId] [int] NULL,
- [KeyResponsibilities] [nvarchar](max) NULL,
- [EducationQualification] [nvarchar](max) NULL,
- CONSTRAINT [PK_RoleId] PRIMARY KEY CLUSTERED
- (
- [RoleId] ASC
- )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
- ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
-
- GO
- SET ANSI_PADDING ON
- 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
- using System;
- using System.Linq;
- using System.Collections.Generic;
- using System.Data.Entity;
- using EntityFramework.MappingAPI;
- using Newtonsoft.Json;
-
- namespace AP.API
- {
- public class Menu
- {
- public IEnumerable<MenuData> GetMenuDetails(string roleName)
- {
- try
- {
- return GetMenuDetailsByRole(roleName);
- }
- catch
- {
- throw;
- }
- return null;
- }
-
- private IEnumerable<MenuData> GetMenuDetailsByRole(string roleName)
- {
- IEnumerable<MenuData> menuData;
- IEnumerable<MenuData> _menuParentNodesData;
-
- using (APEntities hrmsEntities = new APEntities())
- {
- var query = (from menu in hrmsEntities.Menus
- join menuRoles in hrmsEntities.MenuRoles on menu.MenuId equals menuRoles.MenuId
- join roles in hrmsEntities.Roles on menuRoles.RoleId equals roles.RoleId
- where menu.IsActive == true && roles.RoleName == roleName
- orderby menu.DisplayOrder ascending
- select new MenuData
- {
- MenuId = menu.MenuId,
- Title = menu.Title,
- IsActive = menu.IsActive,
- Path = menu.Path,
- DisplayOrder = menu.DisplayOrder,
- ParentId = menu.ParentId,
- Parameter = menu.Parameter,
- NodeId = menu.NodeId,
- Style = menu.Style
- });
- menuData = query.ToList();
-
- if (menuData != null && menuData.Count() > 1)
- {
- _menuParentNodesData = menuData.Where(menu => menu.ParentId == 0);
-
- foreach (var menuItem in _menuParentNodesData)
- {
- buildTreeviewMenu(menuItem, menuData);
- }
- }
- else
- _menuParentNodesData = new MenuData[] {};
- }
- return _menuParentNodesData;
- }
-
- private void buildTreeviewMenu(MenuData menuItem, IEnumerable<MenuData> menudata)
- {
- IEnumerable<MenuData> _menuItems;
-
- _menuItems = menudata.Where(menu => menu.ParentId == menuItem.MenuId);
-
- if (_menuItems != null && _menuItems.Count() > 0)
- {
- foreach (var item in _menuItems)
- {
- menuItem.Categories.Add(item);
- buildTreeviewMenu(item, menudata);
- }
- }
- }
- }
- }
Step 9
This is a DTO class by which the actual data communicates.
MenuData.cs
- public class MenuData:BaseEntity
- {
- public int MenuId { get; set; }
- public string Title { get; set; }
- public string Path { get; set; }
- public int? ParentId { get; set; }
- public int? DisplayOrder { get; set; }
- public string Parameter { get; set; }
- public string NodeId { get; set; }
- public string Style { get; set; }
- public List<MenuData> Categories { get; set; }
- public IEnumerable<RoleData> MenuRoles { get; set; }
-
- public MenuData()
- {
- Categories = new List<MenuData>();
- MenuRoles = new List<RoleData>();
- }
- }
Step 10
MenuController.cs
Here, I have used Web API controller as a Service class.
- using System;
- using AP.API;
- using AP.DomainEntities;
- using System.Collections.Generic;
- using Newtonsoft.Json;
-
- namespace AP.Services.Controllers
- {
- public class MenuController : ApiController
- {
-
-
-
-
-
- [HttpGet]
- public HttpResponseMessage GetMenuDetails(string roleName)
- {
- HttpResponseMessage httpResponseMessage = null;
-
- try
- {
- httpResponseMessage = Request.CreateResponse(new Menu().GetMenuDetails(roleName));
- }
- catch (Exception ex)
- {
- Throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
- {
- Content = new StringContent(ex.Message),
- ReasonPhrase = "Warning"
- });
- }
- return httpResponseMessage;
- }
- }
- }
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
Happy coding.