Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Angular Material Menu: Nested Menu using Dynamic Data
Dharmen Shah
Dharmen Shah

Posted on • Originally published atindepth.dev

     

Angular Material Menu: Nested Menu using Dynamic Data

The Angular Material Menu is a floating panel containing a list of options. In this tutorial, we will learn how we can create nested menus from dynamic data.

We will first learn the basics of Angular Material Menu and how to render a nested menu with a static HTML template.

Then we will understand why and what changes are needed to dynamically render nested menus from data.

Angular Material Menu

<mat-menu> is a floating panel containing a list of options. By itself, the<mat-menu> element does not render anything. The menu is attached to and opened via application of thematMenuTriggerFor directive:

<buttonmat-button[matMenuTriggerFor]="menu">Menu</button><mat-menu#menu="matMenu"><buttonmat-menu-item>Item 1</button><buttonmat-menu-item>Item 2</button></mat-menu>
Enter fullscreen modeExit fullscreen mode

mat-menu output

Static Nested Menu

To render a nested menu with static data, or simply from HTML template, we will have to define the root menu and sub-menus, in addition to setting the[matMenuTriggerFor] on themat-menu-item that should trigger the sub-menu:

<buttonmat-button[matMenuTriggerFor]="animals">Animal index</button><mat-menu#animals="matMenu"><buttonmat-menu-item[matMenuTriggerFor]="vertebrates">Vertebrates</button></mat-menu><mat-menu#vertebrates="matMenu"><buttonmat-menu-item[matMenuTriggerFor]="fish">Fishes</button><buttonmat-menu-item>Amphibians</button><buttonmat-menu-item>Reptiles</button><buttonmat-menu-item>Birds</button><buttonmat-menu-item>Mammals</button></mat-menu><mat-menu#fish="matMenu"><buttonmat-menu-item>Baikal oilfish</button><buttonmat-menu-item>Bala shark</button><buttonmat-menu-item>Ballan wrasse</button><buttonmat-menu-item>Bamboo shark</button><buttonmat-menu-item>Banded killifish</button></mat-menu>
Enter fullscreen modeExit fullscreen mode

And the output will be like below:

static nested mat-menu

Dynamic Nested Menu

Building a menu from dynamic data is often needed, especially in business or enterprise applications. For example, loading features based on logged-in user’s permissions. The data may come from a REST API.

We will take an example where items and their children are loaded from a database. And we will render a nested menu for each item which has children.

Database

For the database, we are going to assume the following service. You can connect the actual REST API with this service, too:

import{Injectable}from"@angular/core";import{delay,of}from"rxjs";@Injectable({providedIn:"root"})exportclassDynamicDatabase{dataMap=newMap<string,string[]>([["Fruits",["Apple","Orange","Banana"]],["Vegetables",["Tomato","Potato","Onion"]],["Apple",["Fuji","Macintosh"]],["Onion",["Yellow","White","Purple"]],["Macintosh",["Yellow","White","Purple"]],]);rootLevelNodes:string[]=["Fruits","Vegetables"];getChildren(node:string){// adding delay to mock a REST API callreturnof(this.dataMap.get(node)).pipe(delay(1000));}isExpandable(node:string):boolean{returnthis.dataMap.has(node);}}
Enter fullscreen modeExit fullscreen mode

Above service’s code is simple:

  • dataMap represents data, this could be the actual database
  • rootLevelNodes represents first nodes to render
  • getChildren will return the items for a particular node. We will use this to render sub-menu items
  • isExpandable will return whether there are any children. We will use this to identify whether a sub-menu is needed

Nested Menu

Now understand that, we can’t simply follow the standard HTML template ofMatMenu for dynamic data. Below are the reasons:

  1. We can’t load the<mat-menu> until we know that item has children
  2. We can’t attach[matMenuTrigger] tomat-menu-item until<mat-menu> is loaded in the DOM

So, to handle the above problems we will follow the below approach in respective order:

  1. Read node from node list
  2. Check if any node is expandable
    1. If yes, then create a sub-menu<mat-menu> with loader and attach it with[matMenuTrigger] in the rendered node’smat-menu-item
      1. Once the user clicks node, get and render child nodes in sub-menu
      2. For sub-menu’s child-nodes, again follow the same approach and start from step 2
    2. If no, then simply create node’smat-menu-item

Root Component

To achieve the above approach, we will create aapp-menu component and use it inapp-root:

<!-- src/app/app.component.html --><app-menu[trigger]="'Food'"[data]="initialData"[isRootNode]="true"></app-menu>
Enter fullscreen modeExit fullscreen mode
// src/app/app.component.tsimport{Component}from"@angular/core";import{DynamicDatabase}from"./dynamic-database.service";@Component({selector:"app-root",templateUrl:"app.component.html",})exportclassAppComponent{title="mat-menu-dynamic-data";initialData:string[]=[];constructor(privatedatabase:DynamicDatabase){this.initialData=this.database.rootLevelNodes.slice();}}
Enter fullscreen modeExit fullscreen mode

We are readingrootLevelNodes and passing it asdata inapp-menu.

Menu Component

For the menu, initially we want to show a button, which will trigger a menu:

<!-- src/app/menu/menu.component.html --><buttonmat-button[matMenuTriggerFor]="menu">  {{ trigger }}</button><mat-menu#menu="matMenu"><buttonmat-menu-item*ngFor="let node of data">{{ node }}</button></mat-menu>
Enter fullscreen modeExit fullscreen mode

And the class looks like this:

// src/app/menu/menu.component.tsexportclassMenuComponent{@Input()data:string[]=[];@Input()trigger="Trigger";@Input()isRootNode=false;}
Enter fullscreen modeExit fullscreen mode

Recursion

Now, to render a nested menu, we will just need to handle recursion in this code. And generate the same DOM structure for each nested menu.

So, first we will change the code inside<mat-menu>:

<!-- src/app/menu/menu.component.html --><buttonmat-button[matMenuTriggerFor]="menu">  {{ trigger }}</button><mat-menu#menu="matMenu"><ng-container*ngFor="let node of data; let i = index"><buttonmat-menu-item><app-menu[trigger]="node"*ngIf="isExpandable(node); else menuItem"></app-menu></button><ng-template#menuItem><buttonmat-menu-item>{{ node }}</button></ng-template></ng-container></mat-menu>
Enter fullscreen modeExit fullscreen mode

Now, inside the menu, we are checking for each node, if theisExpandable method returnstrue, we are renderingapp-menu again inside it.

isExpandable method will simply callisExpandable from theDynamicDatabase service:

// src/app/menu/menu.component.ts// ...exportclassMenuComponent{// ...isExpandable(node:string):boolean{returnthis.database.isExpandable(node);}}
Enter fullscreen modeExit fullscreen mode

Let’s look at the output:

menu component root trigger output

Notice that text is also hoverable insidemat-menu-item. That’s because of themat-button. Whenapp-menu is rendered inside, we will have to change the directive of the button frommat-button tomat-menu-item, let’s do that:

<!-- src/app/menu/menu.component.html --><button*ngIf="isRootNode"mat-button[matMenuTriggerFor]="menu">  {{ trigger }}</button><button*ngIf="!isRootNode"mat-menu-item[matMenuTriggerFor]="menu">  {{ trigger }}</button><mat-menu#menu="matMenu"><ng-container*ngFor="let node of data; let i = index"><buttonmat-menu-item><app-menu[trigger]="node"*ngIf="isExpandable(node); else menuItem"></app-menu></button><ng-template#menuItem><buttonmat-menu-item>{{ node }}</button></ng-template></ng-container></mat-menu>
Enter fullscreen modeExit fullscreen mode

Let’s look at the output now:

menu component with nested menu trigger output

It’s rendering the root items fine now, but the sub-menu is blank. Let’s add data in it.

Data

We want to load the data once the menu is rendered and opened. So, we will use the(menuOpened) event to load thedata.menuOpened emits the event when the associated menu is opened.

We only want to load thedata for non-root items, because for root items,data is coming from the parent component.

<!-- src/app/menu/menu.component.html --><button*ngIf="isRootNode"mat-button[matMenuTriggerFor]="menu">  {{ trigger }}</button><button*ngIf="!isRootNode"mat-menu-item[matMenuTriggerFor]="menu"(menuOpened)="getData(trigger)">  {{ trigger }}</button><!-- rest remains same -->
Enter fullscreen modeExit fullscreen mode

Let’s create agetData method inmenu.component.ts:

// src/app/menu/menu.component.ts// ...exportclassMenuComponent{// ...isLoading=false;dataLoaded=false;getData(node:string){if(!this.dataLoaded){this.isLoading=true;this.database.getChildren(node).subscribe((d)=>{this.data=d?.slice()||[];this.isLoading=false;this.dataLoaded=true;});}}}
Enter fullscreen modeExit fullscreen mode

WithgetData, we are creating 2 more flags:

  1. isLoading - Indicates ifdata is being fetched
  2. dataLoaded - Indicates ifdata is already loaded and prevents further fetching

Let’s look at the output now:

menu component with dynamic data

Notice that data is getting loaded after a particular time, that’s because we have added adelay inDynamicDatabase.getChildren to simulate an API call. And it’s not fetching the data again if it’s already loaded and in that case menu items are rendered instantly.

Loader

The last thing remaining is to show a loader whendata is getting fetched. We already haveisLoading flag, let’s use that to show<mat-spinner>:

<!-- src/app/menu/menu.component.html --><!-- rest remains same --><mat-menu#menu="matMenu"><buttonmat-menu-item*ngIf="isLoading"style="display: flex; justify-content: center; align-items: center"><mat-spinnermode="indeterminate"diameter="24"></mat-spinner></button><ng-container*ngFor="let node of data; let i = index"><!-- rest remains same --></ng-container></mat-menu>
Enter fullscreen modeExit fullscreen mode

Notice that I have added some inline styles so that<mat-spinner> is displayed in the center ofmat-menu-item.

Let’s look at the output now:

nested menu with loader

Summary

We started with a simple example of a menu, where we rendered nested menus using static HTML template.

Then we understood the need for dynamic data in nested menus and the problems to achieve dynamicity with the simple HTML template.

We then created aapp-menu component. First we loaded a menu with root items, provided asdata input from the parent component.

Then we handled recursion, renderingapp-menu insideapp-menu, based onisExpandable flag. Next we implemented fetching data based onmenuOpened event and finally we displayed a loader while fetching the data.

All the above code is available on GitHub repo:mat-menu-dynamic-data.

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I work on Angular, React, Electron, JavaScript, CSS, SCSS, Bootstrap, NodeJS. Love to contribute to Open-Source Projects.
  • Location
    Ahmedabad
  • Education
    Bechelor of Technology
  • Work
    Sr. Full-Stack Developer at Solvative
  • Joined

More fromDharmen Shah

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp