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>