使用 Angular 5 从 JSON 对象创建动态嵌套 Material 菜单

37

如何从JSON对象创建动态嵌套菜单?

我今天第一次开始使用Angular Material Design,并尝试使用Material Design创建嵌套菜单。对于静态内容,文档非常直接。

但是,我需要从JSON对象创建动态嵌套菜单,而我无法在任何地方找到简单的解决方案。它只需要深入一层。

JSON对象(不是固定的):

my_menu = {
    'main1': ['sub1', 'sub2'],
    'main2': ['sub1', 'sub2'],
}

使用动态方式可以生成以下内容:在stackblitz上的期望结果示例

外观

我尝试使用*ngFor来构建主菜单,然后再单独处理每个子菜单,但这样做会导致错误。

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

我知道这是错的,但那就是我的Angular理解的尽头。

Object.keys只是返回对象的所有键,它使用从ts文件加载的Object.keys函数。

objectKeys = Object.keys;

顺便提一句,我对Angular也不是很熟悉

2个回答

56
以下结构应该对您有用:
<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>

由于我将sub_menu放在嵌入式模板(*ngFor)中,我们可以使用相同的名称作为模板引用变量(#sub_menu)。

Stackblitz示例


1
非常感谢您的回答。 我将扩展问题,并在此与您保持一致。 如果我有一个带有子项数组和一个没有子项的主项, 如何以动态方式使没有任何子项的主项中的箭头消失?@yurzui - Ezri Y
忽略代码风格,我必须承认这个解决方案背后的思路非常优美。为了实现类似的效果,我用TemplateOutlets和ngContainers的混合来总结不同菜单项类型的顶部菜单,但是这里的片段是重构我的代码的充分理由 :) 谢谢! - Javatheist
顺便说一下... objectKeys... 同时令人惊叹和印象深刻,不错 :) - Javatheist
如何将此扩展为“n”级深度嵌套菜单? - Phalgun

23
更新:重构了基于JSON的“任意深度嵌套”示例,因为它在Angular 12中不再起作用。这是一个基于这篇好文章的工作中的Angular 13 StackBlitz示例
为了使其正常工作,我将菜单触发按钮移动到菜单项组件内部,以便每个菜单项组件实例中只有一个菜单。
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>

菜单项组件.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

->

app组件的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>

这里是一个 StackBlitz 示例,它基于 JSON 构建了任意深度的嵌套(由 @Splaktar 撰写)

任意嵌套的关键在于自我引用的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>

2
注意:最好使用Angular提供的ng-container元素,这样您就不会在菜单的同一父级中有多个<span>元素。 - Edric
1
这应该被标记为正确答案,你刚刚通过这个实现救了我,我从来没有想过在组件内部调用它自己。 - Danny908
2
为了让它在 Angular 10 中正常工作,我不得不在 viewChild 中添加 static: true - Kotohitsu
@Kotohitsu,你有可工作的示例吗? - ievgen
1
在 Angular 12 中,这个功能已经失效了。菜单不再自动打开/关闭。 - ievgen
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接