Dynamic nested Material menu from json object in Angular 5
Solution 1
The following structure should work for you:
<button mat-button [matMenuTriggerFor]="main_menu">My menu</button>
<mat-menu #main_menu="matMenu">
<ng-container *ngFor="let mainItem of objectKeys(my_menu)">
<button mat-menu-item [matMenuTriggerFor]="sub_menu">{{ mainItem }}</button>
<mat-menu #sub_menu="matMenu">
<button *ngFor="let subItem of my_menu[mainItem]" mat-menu-item>{{ subItem }}</button>
</mat-menu>
</ng-container>
</mat-menu>
Since I placed sub_menu
inside the embedded template (*ngFor
) we can use the same name for template reference variable(#sub_menu
).
Solution 2
Update: Reworked the "arbitrarily deep nesting based on JSON" example since it was no longer working in Angular 12. Here is a working Angular 13 StackBlitz example based on this great article
To get it working, I moved the menu trigger button inside the menu-item component so there is only one menu in each instance of menu-item component.
menu-item.component.html
<mat-menu #menu="matMenu" [overlapTrigger]="false">
<span *ngFor="let child of children">
<!-- Handle branch node buttons here -->
<ng-container *ngIf="child.children && child.children.length > 0">
<app-menu-item [item]="child" [children]="child.children"></app-menu-item>
</ng-container>
<!-- Leaf node buttons here -->
<ng-container *ngIf="!child.children || child.children.length === 0">
<button mat-menu-item color="primary" [routerLink]="child.route">
{{ child.displayName }}
</button>
</ng-container>
</span>
</mat-menu>
<button
mat-menu-item
color="primary"
[matMenuTriggerFor]="menu"
[disabled]="item.disabled"
>
<mat-icon>{{ item.iconName }}</mat-icon>
{{ item.displayName }}
</button>
menu-item.component.ts
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NavItem } from '../nav-item';
@Component({
selector: 'app-menu-item',
templateUrl: './menu-item.component.html',
styleUrls: ['./menu-item.component.css'],
})
export class MenuItemComponent implements OnInit {
@Input() children: NavItem[];
@Input() item: NavItem;
constructor(public router: Router) {}
ngOnInit() {}
}
app.component.html
<div class="basic-container">
<mat-toolbar class="menu-bar mat-elevation-z1">
<span *ngFor="let item of navItems">
<!-- Handle branch node buttons here -->
<ng-container *ngIf="item.children && item.children.length > 0">
<app-menu-item [item]="item" [children]="item.children"></app-menu-item>
</ng-container>
<!-- Leaf node buttons here -->
<ng-container *ngIf="!item.children || item.children.length === 0">
<button mat-button color="primary" [routerLink]="item.route">
{{ item.displayName }}
</button>
</ng-container>
</span>
</mat-toolbar>
<router-outlet></router-outlet>
</div>
Here is a StackBlitz example of an arbitrarily deep nesting based on JSON (authored by @Splaktar)
The key to arbitrary nesting is the self-referencing menu-item.component:
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {Router} from '@angular/router';
import {NavItem} from '../nav-item';
@Component({
selector: 'app-menu-item',
templateUrl: './menu-item.component.html',
styleUrls: ['./menu-item.component.scss']
})
export class MenuItemComponent implements OnInit {
@Input() items: NavItem[];
@ViewChild('childMenu') public childMenu;
constructor(public router: Router) {
}
ngOnInit() {
}
}
<mat-menu #childMenu="matMenu" [overlapTrigger]="false">
<span *ngFor="let child of items">
<!-- Handle branch node menu items -->
<span *ngIf="child.children && child.children.length > 0">
<button mat-menu-item color="primary" [matMenuTriggerFor]="menu.childMenu">
<mat-icon>{{child.iconName}}</mat-icon>
<span>{{child.displayName}}</span>
</button>
<app-menu-item #menu [items]="child.children"></app-menu-item>
</span>
<!-- Handle leaf node menu items -->
<span *ngIf="!child.children || child.children.length === 0">
<button mat-menu-item [routerLink]="child.route">
<mat-icon>{{child.iconName}}</mat-icon>
<span>{{child.displayName}}</span>
</button>
</span>
</span>
</mat-menu>
![rain01](https://i.stack.imgur.com/ZMDj6.jpg?s=256&g=1)
rain01
Experienced Full Stack Engineer with a demonstrated history of working in the internet industry.
Updated on August 26, 2021Comments
-
rain01 almost 3 years
How to create dynamic nested menu from json object?
I started using Angular Material Design today for the first time and I'm trying to create nested menus using material design. The documentation is pretty straight forward for static stuff.
But I need to create dynamic nested menu from json object and I can't find a simple solution to this anywhere. It just needs to be one level deep.
json object(not set in stone):
my_menu = { 'main1': ['sub1', 'sub2'], 'main2': ['sub1', 'sub2'], }
which would generate something like this but dynamically: expected result example at stackblitz
I tried building it running
*ngFor
like this for main menu and then separate on each sub menu but it ended in errors.<button mat-button [matMenuTriggerFor]="main_menu">My menu</button> <mat-menu #main_menu="matMenu"> <button *ngFor="let main_item of objectKeys(my_menu)" mat-menu-item [matMenuTriggerFor]="main_item">{{ main_item }}</button> <button mat-menu-item [matMenuTriggerFor]="main2">main2</button> </mat-menu> <mat-menu *ngFor="let sub_menu of objectKeys(my_menu)" #sub_menu="matMenu"> <button *ngFor="let sub_name of sub_menu" mat-menu-item>{{ sub_name }}</button> </mat-menu>
I know it's wrong but that's where my understanding of angular ended.
objectKeys just returns all the keys of the object using
Object.keys
which is loaded from the ts file.objectKeys = Object.keys;
PS. I'm fairly new to Angular also
-
Ezri Y over 5 yearsthank you very much for your answer. I will expand the question, for this, I will consist with your names. If I had one mainItem with subItem array and one without subItems how can I disappear the arrow from the mainItem that has no any sub-items in a dynamic way? @yurzui
-
SilverFish about 5 yearsI am also looking for building Dynamic menus/tabs in Angular 6/7 application which I need to develop. I have some knowledge of Angular and have build routes (predefined), but haven't used Angular material yet. Please advise do we need Angular material to accomplish this kind of functionality or is it possible to achieve it without it as well? In addition, I need both horizontal and vertical menus/submenus.
-
Clem about 5 yearsI wrote one library to handle dynamic rendering of menus, which you can find here: github.com/klemenoslaj/ng-action-outlet And demo: stackblitz.com/edit/ng-action-outlet-demo
-
Edric almost 5 yearsNote: It would be a good idea to use the
ng-container
element that Angular provides such that you wouldn't have multiple<span>
elements in the same parent of the menu. -
Danny908 over 4 yearsThis should be the marked as the correct answer, You just saved me with this implementation, I have never thought call a component inside of itself.
-
Javatheist over 4 yearsNeglecting the code style, I gotta admit that the idea behind this solution is beautiful. In order to achieve something similar with different menu-item types on top, I concluded it with a mix of TemplateOutlets and ngContainers, but this snippet here is a sound reason to refactor my code :) thanks!
-
Javatheist over 4 yearsbtw... objectKeys... audacious and impressive at the same time, nice one :)
-
Admin over 3 yearsTo get it working in angular 10 I had to add
static: true
to theviewChild
-
ievgen about 2 years@Kotohitsu do you have a working example ?
-
ievgen about 2 yearsthis no longer works in Angular 12. Menus no longer open / close automatically
-
Datum Geek about 2 yearsUpdated answer with working Angular 13 stackblitz example :)