37

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

rain01
  • 1,194
  • 1
  • 16
  • 25

2 Answers2

56

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

nephiw
  • 1,964
  • 18
  • 38
yurzui
  • 205,937
  • 32
  • 433
  • 399
  • 1
    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 – Ezri Y Dec 24 '18 at 15:13
  • 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 Feb 29 '20 at 17:53
  • btw... objectKeys... audacious and impressive at the same time, nice one :) – Javatheist Feb 29 '20 at 17:59
  • How can this be extended for "n" levels deep nested menu? – Phalgun Aug 28 '23 at 05:30
23

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>
Datum Geek
  • 1,358
  • 18
  • 23
  • 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. – SilverFish May 23 '19 at 14:05
  • I wrote one library to handle dynamic rendering of menus, which you can find here: https://github.com/klemenoslaj/ng-action-outlet And demo: https://stackblitz.com/edit/ng-action-outlet-demo – Clem Jun 13 '19 at 08:00
  • 2
    Note: It would be a good idea to use the `ng-container` element that Angular provides such that you wouldn't have multiple `` elements in the same parent of the menu. – Edric Aug 15 '19 at 14:06
  • 1
    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. – Danny908 Oct 28 '19 at 18:02
  • 2
    To get it working in angular 10 I had to add `static: true` to the `viewChild` – Kotohitsu Dec 02 '20 at 07:41
  • @Kotohitsu do you have a working example ? – ievgen Apr 02 '22 at 08:23
  • 1
    this no longer works in Angular 12. Menus no longer open / close automatically – ievgen Apr 02 '22 at 08:24
  • Updated answer with working Angular 13 stackblitz example :) – Datum Geek Apr 10 '22 at 10:56