如何基于Angular CDK Menu实现JSON驱动的动态递归菜单栏
Angular CDK 动态递归菜单自动关闭失效问题修复
问题根因
使用递归自定义组件渲染多级菜单时,组件的宿主标签会打断菜单项的DOM层级关系:所有带cdkMenuItem指令的元素不再是cdkMenu/cdkMenuBar的直接子节点,CDK菜单无法识别同级菜单触发器,因此鼠标切换菜单项时不会自动关闭已展开的其他菜单。
原有实现逻辑:
- 根层使用
cdkMenuBar作为菜单栏容器,循环挂载自定义菜单组件
<div class="titlebar" cdkMenuBar> <app-menu *ngFor="let item of menu" [item]="item" [isRoot]="true"></app-menu> </div>
- 自定义菜单组件内部递归渲染子项,模板代码如下:
<button class="menu-item-button" cdkMenuItem [cdkMenuTriggerFor]="menu" [class.menubar-button]="isRoot" > <div>{{ item.label }}</div> <div *ngIf="!isRoot && item.type === 'submenu'">></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">></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
相关产品推荐
相关产品推荐

