You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Angular CDK Menu实现JSON驱动的动态递归菜单栏

Angular CDK 动态递归菜单自动关闭失效问题修复

问题根因

使用递归自定义组件渲染多级菜单时,组件的宿主标签会打断菜单项的DOM层级关系:所有带cdkMenuItem指令的元素不再是cdkMenu/cdkMenuBar的直接子节点,CDK菜单无法识别同级菜单触发器,因此鼠标切换菜单项时不会自动关闭已展开的其他菜单。
原有实现逻辑:

  1. 根层使用cdkMenuBar作为菜单栏容器,循环挂载自定义菜单组件
<div class="titlebar" cdkMenuBar>
  <app-menu *ngFor="let item of menu" [item]="item" [isRoot]="true"></app-menu>
</div>
  1. 自定义菜单组件内部递归渲染子项,模板代码如下:
<button
  class="menu-item-button"
  cdkMenuItem
  [cdkMenuTriggerFor]="menu"
  [class.menubar-button]="isRoot"
>
  <div>{{ item.label }}</div>
  <div *ngIf="!isRoot && item.type === 'submenu'">&gt;</div>
</button>

<ng-template #menu>
  <div class="menu-dropdown" cdkMenu>
    <ng-container *ngFor="let subItem of item.submenu">
      <ng-container *ngIf="subItem.type !== 'separator'; else separator">
        <app-menu
          [item]="subItem"
          *ngIf="subItem.type === 'submenu'; else menuItem"
        ></app-menu>
        <ng-template #menuItem>
          <button class="menu-item-button" cdkMenuItem>
            <div>{{ subItem.label }}</div>
            <div class="text-color-tertiary">
              {{ subItem.accelerator }}
            </div>
          </button>
        </ng-template>
      </ng-container>
      <ng-template #separator>
        <hr />
      </ng-template>
    </ng-container>
  </div>
</ng-template>

最优解决方案

放弃自定义组件递归的实现方式,改用ng-template自调用完成递归渲染,保证所有CDK菜单指令绑定的元素都直接处于菜单容器的DOM层级下,不被自定义组件标签打断。
实现代码如下:

<!-- 根菜单栏 -->
<div class="titlebar" cdkMenuBar>
  <ng-container *ngFor="let item of menu">
    <ng-container *ngTemplateOutlet="menuRenderTpl; context: { $implicit: item, isRoot: true }"></ng-container>
  </ng-container>
</div>

<!-- 递归菜单模板 -->
<ng-template #menuRenderTpl let-item let-isRoot="isRoot">
  <!-- 分隔线 -->
  <hr *ngIf="item.type === 'separator'" />

  <!-- 普通可点击菜单项 -->
  <button 
    *ngIf="item.type !== 'submenu' && item.type !== 'separator'" 
    class="menu-item-button" 
    cdkMenuItem
    (cdkMenuItemTriggered)="item.handler?.()"
  >
    <div>{{ item.label }}</div>
    <div class="text-color-tertiary" *ngIf="item.accelerator">{{ item.accelerator }}</div>
  </button>

  <!-- 带子级的菜单项 -->
  <ng-container *ngIf="item.type === 'submenu'">
    <button
      class="menu-item-button"
      cdkMenuItem
      [cdkMenuTriggerFor]="subMenuPanel"
      [class.menubar-button]="isRoot"
    >
      <div>{{ item.label }}</div>
      <div *ngIf="!isRoot">&gt;</div>
    </button>
    <ng-template #subMenuPanel>
      <div class="menu-dropdown" cdkMenu>
        <ng-container *ngFor="let subItem of item.submenu">
          <ng-container *ngTemplateOutlet="menuRenderTpl; context: { $implicit: subItem, isRoot: false }"></ng-container>
        </ng-container>
      </div>
    </ng-template>
  </ng-container>
</ng-template>

该实现完全符合Angular CDK Menu的DOM层级要求,鼠标hover切换菜单项时,CDK会自动关闭已展开的其他菜单,所有原生交互逻辑都能正常运行。
如果必须拆分独立组件做逻辑复用,需要在自定义组件的宿主元素上绑定cdkMenuItem系列指令,同时通过依赖注入把组件内的菜单实例注册到父级菜单的控制栈中,实现成本较高,非必要不推荐。


内容的提问来源于stack exchange,提问作者John Woodruff

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 01:39:17