Dynamic nested Material menu from json object in Angular 5

34,487

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).

Stackblitz Example

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>
Share:
34,487
rain01
Author by

rain01

Experienced Full Stack Engineer with a demonstrated history of working in the internet industry.

Updated on August 26, 2021

Comments

  • rain01
    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

    how it looks

    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
    Ezri Y over 5 years
    thank 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
    SilverFish about 5 years
    I 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
    Clem about 5 years
    I 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
    Edric almost 5 years
    Note: 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
    Danny908 over 4 years
    This 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
    Javatheist over 4 years
    Neglecting 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
    Javatheist over 4 years
    btw... objectKeys... audacious and impressive at the same time, nice one :)
  • Admin
    Admin over 3 years
    To get it working in angular 10 I had to add static: true to the viewChild
  • ievgen
    ievgen about 2 years
    @Kotohitsu do you have a working example ?
  • ievgen
    ievgen about 2 years
    this no longer works in Angular 12. Menus no longer open / close automatically
  • Datum Geek
    Datum Geek about 2 years
    Updated answer with working Angular 13 stackblitz example :)