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

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

4 Comments

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
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!
btw... objectKeys... audacious and impressive at the same time, nice one :)
How can this be extended for "n" levels deep nested menu?
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>

9 Comments

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.
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.
To get it working in angular 10 I had to add static: true to the viewChild
this no longer works in Angular 12. Menus no longer open / close automatically
Updated answer with working Angular 13 stackblitz example :)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.