May 31, 2023 7 mins

How to create Angular material nested side menu

Angular material nested side menu Angular material nested side menu

A side menu is a common part of many web applications, offering users a way to navigate through different sections. In this article, we'll look into the process of making a nested side menu using Angular Material. By making the menu dynamic and nested, we can make it more adaptable and scalable, using the capabilities of Angular and its features.

This article is the part 5 of the Angular Material tutorial series. We’re about to start working on the nested side menu from where we left off in the previous tutorial. To make things easier, clone the starting template for this Angular material app from here.

To get the most out of this tutorial, it’s recommended to review the previous ones. Understand how we created the responsive Angular side navigation(part 1), responsive material dashboard(part 2), responsive material data tables(part 3), and Angular material theming(part 4). While Parts 2, 3, and 4 aren’t crucial for this tutorial, it’s a good idea to at least go through Part 1 to grasp the basic features of our Angular Material app.

What we are going to develop?

In this tutorial, our main focus is on creating a nested side menu, and you can get a preview of what we’ll be working on in this video. The tutorial begins by building a route config object that includes all the nested routes we plan to implement. Afterward, we’ll incorporate this object into the side navigation components. The video provides a clear look at the end result, showcasing the nested side menu in both the closed and open states of the side navigation bar. It’s important to note that the implementation is designed to be fully responsive on mobile devices, ensuring a seamless user experience across various screen sizes.

Understanding the Concept of a Dynamic Nested Side Menu

A dynamic nested side menu is a menu structure that can be generated dynamically based on data retrieved from an API or any other data source. It allows for the hierarchical organization of menu items, enabling submenus within submenus. This flexibility enables developers to create menus that can adapt to changing requirements and provide a seamless user experience.

Create the nested menu data source

In order to create a dynamic menu, we need to fetch menu items (data) from an API or some data source. In this tutorial, our goal is to establish a personalized data source by modifying the child-routes.ts file. These modifications align with the specific requirements of the nested side menu we intend to implement.

Firstly, we’ll create a TypeScript interface named AppRoute with certain properties. If you’re not familiar with Angular (TypeScript) interfaces, you can refer to this tutorial.

The AppRoute interface assists in maintaining a consistent data structure for AppRoutes throughout the application. The properties include,

  • path (mandatory)
  • component (optional)
  • data (mandatory)
  • children (optional)

Our data source, named childRoutes, is an array of type AppRoute. The structure of the data source is easily understandable. Once all the changes are made, the final version of the child-routes.ts file looks like the code below.

import { FormComponent } from './form/form.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { TableComponent } from './table/table.component';

export interface AppRoute {
  path: string;
  component?: any;
  data: any;
  children?: AppRoute[];
}

export const childRoutes: AppRoute[] = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    data: { icon: 'dashboard', text: 'Dashboard' }
  },
  {
    path: 'table',
    component: TableComponent,
    data: { icon: 'table', text: 'Table' }
  },
  {
    path: 'menu',
    data: { text: 'Menu' },
    children: [
      {
        path: 'form',
        component: FormComponent,
        data: { text: 'Menu1' },
        children: [
          {
            path: 'form',
            component: FormComponent,
            data: { icon: 'insert_chart', text: 'Menu2' }
          },
          {
            path: 'form',
            component: FormComponent,
            data: { icon: 'format_color_fill', text: 'Menu3' },
            children: [
              {
                path: 'form',
                component: FormComponent,
                data: { icon: 'library_add', text: 'Menu4' }
              },
              {
                path: 'form',
                component: FormComponent,
                data: { icon: 'equalizer', text: 'Menu5' },
                children: [
                  {
                    path: 'form',
                    component: FormComponent,
                    data: { icon: 'import_contacts', text: 'Menu6' }
                  },
                  {
                    path: 'form',
                    component: FormComponent,
                    data: { icon: 'list_alt', text: 'Menu7' }
                  },
                  {
                    path: 'form',
                    component: FormComponent,
                    data: { icon: 'business', text: 'Menu8' }
                  },
                  {
                    path: 'form',
                    component: FormComponent,
                    data: { icon: 'tab', text: 'Menu9' }
                  }
                ]
              }
            ]
          }
        ]
      },
      {
        path: 'form',
        component: FormComponent,
        data: { icon: 'wallpaper', text: 'Menu10' }
      }
    ]
  },
  {
    path: 'form',
    component: FormComponent,
    data: { icon: 'bar_chart', text: 'Form' }
  },
];

This TypeScript file defines the structure and properties of our nested menu data source.

To implement the nested side menu, we need to organize the menu items hierarchically. The menu items can be represented as a tree-like structure, where each item can have child items. Later, we will feed this data source into the side nav component that renders the menu items and their submenus dynamically.

Import required modules for the nested side menu

To enable the creation of our nested side menu using Angular Material, we need to import the necessary modules. In the material.module.ts file, we incorporate the CdkTreeModule from the Angular material library. The updated file is as follows.

import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { MatTableModule } from '@angular/material/table';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatMenuModule } from '@angular/material/menu';
import { MatTableExporterModule } from 'mat-table-exporter';
import { MatDialogModule } from '@angular/material/dialog';
import { CdkTreeModule } from '@angular/cdk/tree';

@NgModule({
    imports: [
        MatButtonModule,
        MatToolbarModule,
        MatIconModule,
        MatSidenavModule,
        MatListModule,
        MatSlideToggleModule,
        MatCardModule,
        MatPaginatorModule,
        MatSortModule,
        MatInputModule,
        MatTableModule,
        MatFormFieldModule,
        MatCheckboxModule,
        MatProgressSpinnerModule,
        MatSelectModule,
        MatMenuModule,
        MatTableExporterModule,
        MatDialogModule,
        CdkTreeModule
    ],
    exports: [
        MatButtonModule,
        MatToolbarModule,
        MatIconModule,
        MatSidenavModule,
        MatListModule,
        MatSlideToggleModule,
        MatCardModule,
        MatPaginatorModule,
        MatSortModule,
        MatInputModule,
        MatTableModule,
        MatFormFieldModule,
        MatCheckboxModule,
        MatProgressSpinnerModule,
        MatSelectModule,
        MatMenuModule,
        MatTableExporterModule,
        MatDialogModule,
        CdkTreeModule
    ]
})

export class MaterialModule {}

This modification ensures that the necessary Angular Material modules, including CdkTreeModule, are available for use in our nested side menu implementation.

Configure opened side nav component

To configure the opened side navigation component, we’ll begin by importing the necessary data and modules. This includes the childRoutes data source and AppRoute interface from the child-routes.ts file, as well as ArrayDataSource, and NestedTreeControl from the Angular Material CDK module. Following that, we’ll create a data source object utilizing the childRoutes.

For building the opened nested side menu, we’ll leverage the existing side-nav component. The HTML markup of this component displays the title and icon of the menu item. Within this markup, mat-nav-list, cdk-tree, cdk-nested-tree-node, and mat-list-item are utilized to establish the nested menu.

After making the necessary changes, the final version of the side nav component is represented in the TypeScript code below.

import { Component, OnInit } from '@angular/core';
import { childRoutes, AppRoute } from '../../child-routes';
import { ArrayDataSource } from '@angular/cdk/collections';
import { NestedTreeControl } from '@angular/cdk/tree';

@Component({
  selector: 'app-side-nav',
  templateUrl: './side-nav.component.html',
  styleUrls: ['./side-nav.component.scss']
})
export class SideNavComponent implements OnInit {
  treeControl = new NestedTreeControl<AppRoute>(node => node.children);

  // create data source object 
  dataSource = new ArrayDataSource(childRoutes);
  hasChild = (_: number, node: AppRoute) => !!node.children && node.children.length > 0;
  constructor(
    ) {}

  ngOnInit() {}
}

The corresponding HTML and CSS code for the side nav component is structured as follows.

<mat-nav-list>
  <cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
    <cdk-nested-tree-node *cdkTreeNodeDef="let node" class="app-tree-node">
      <a mat-list-item [routerLinkActive]="'active-link'" [routerLink]="['/', node.path]">
        <mat-icon color="primary" class="sidenav-icon">{{ node.data.icon }}</mat-icon>
        <span class="sidenav-text">{{ node.data.text }}</span>
      </a>
    </cdk-nested-tree-node>
    <cdk-nested-tree-node *cdkTreeNodeDef="let node; when: hasChild" class="app-tree-node">
      <a mat-list-item [attr.aria-label]="'Toggle ' + node.data.text" cdkTreeNodeToggle>
        <mat-icon color="primary" class="mat-icon-rtl-mirror">
          {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
        </mat-icon>
        <span class="sidenav-text">{{ node.data.text }}</span>
      </a>
      <div [class.app-tree-invisible]="!treeControl.isExpanded(node)">
        <ng-container cdkTreeNodeOutlet></ng-container>
      </div>
    </cdk-nested-tree-node>
  </cdk-tree>
</mat-nav-list>

And the associated CSS code is structured as follows.

.active-link {
  font-weight: bold !important;
}

.mat-nav-list {
  width: 100%;
  a {
    .mat-icon {
      margin-right: 18px;
    }
    display: block;
  }
}

.app-tree-invisible {
  display: none;
}

.app-tree ul,
.app-tree li {
  margin-top: 0;
  margin-bottom: 0;
  list-style-type: none;
}

.app-tree-node {
  display: block;
}

.app-tree-node .app-tree-node {
  padding-left: 20px;
}

.sidenav-text{
  vertical-align: super;
  margin-left: 5px;
}

Configure closed-side nav component

To configure the closed-side navigation component, we will replicate the same setup as in the side-nav component. However, in the markup of the side-nav-closed component, we will hide the menu text (title) and only display the menu icon. Additionally, we will adjust the padding-left of the .app-tree-node .app-tree-node classes to 0px, enabling the virtual expansion of submenus. The final version of the side-nav-closed component is outlined below.

In the TypeScript code.

import { Component, OnInit } from '@angular/core';
import { childRoutes, AppRoute } from '../../child-routes';
import { ArrayDataSource } from '@angular/cdk/collections';
import { NestedTreeControl } from '@angular/cdk/tree';

@Component({
  selector: 'app-side-nav-closed',
  templateUrl: './side-nav-closed.component.html',
  styleUrls: ['./side-nav-closed.component.scss']
})
export class SideNavClosedComponent implements OnInit {
  treeControl = new NestedTreeControl<AppRoute>(node => node.children);
  dataSource = new ArrayDataSource(childRoutes);
  hasChild = (_: number, node: AppRoute) => !!node.children && node.children.length > 0;
  constructor() { }

  ngOnInit(): void {
  }

}

In the HTML markup.

<mat-nav-list>

  <cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">

    <cdk-nested-tree-node *cdkTreeNodeDef="let node" class="app-tree-node">
      <a mat-list-item [routerLinkActive]="'active-link'" [routerLink]="['/', node.path]">
        <mat-icon color="primary" class="sidenav-icon">{{ node.data.icon }}</mat-icon>
      </a>
    </cdk-nested-tree-node>

    <cdk-nested-tree-node *cdkTreeNodeDef="let node; when: hasChild" class="app-tree-node">
      <a mat-list-item [attr.aria-label]="'Toggle ' + node.data.text" cdkTreeNodeToggle>
      <mat-icon color="primary" class="mat-icon-rtl-mirror">
        {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
      </mat-icon>
      </a>

      <div [class.app-tree-invisible]="!treeControl.isExpanded(node)">
        <ng-container cdkTreeNodeOutlet></ng-container>
      </div>

    </cdk-nested-tree-node>
  </cdk-tree>
</mat-nav-list>

In the CSS file.

@import '../side-nav/side-nav.component.scss';

.app-tree-node .app-tree-node {
  padding-left: 0px;
}

To further enhance the customization options, we can add more styles in the nested side nav menu.

Handling Menu Item Selection

When a menu item is selected, we may want to trigger some action or navigate to a specific page within the application. Angular provides various techniques for handling user interactions. In our case, we will use the routerLink property.  It will load the correct component based on the selected route. Apart from that, we will use the routerLinkActive property to highlight the selected menu item in the Angular material nested side menu.

Conclusion

Developing a dynamic nested side menu in Angular provides a practical solution for building flexible and scalable navigation systems in Angular applications. Leveraging the capabilities of Angular Material’s built-in components streamlines the development process, contributing to increased efficiency in creating interactive and user-friendly interfaces for web applications. This approach ensures that the navigation structure is adaptable to various requirements, allowing for a more straightforward and effective implementation within the Angular framework.


Comments


There are no comments yet.

Write a comment

You can use the Markdown syntax to format your comment.