我正在开发一个具有可配置菜单结构的样板项目。其中的菜单绑定到递归数据类型。我想通过 ng-container 和 ng-template 来解决这个问题,因为在我看来,这是递归执行某些步骤的唯一方法。
我已经使用材质开发了几个组件,所以我也想坚持使用材质组件,而不是使用其他菜单组件。
我遇到的问题是,如果 mat-menu 项是在 ng-template 中定义的,则菜单无法正确呈现。 [https://i.imgur.com/WQed1yb.gif]
菜单项的菜单触发似乎没有按应有的方式呈现。它缺少子菜单图标,将鼠标悬停在其上不会显示子菜单。相反,需要单击一下。此外,当子菜单打开时,父菜单会消失,这会使菜单导航变得复杂。
除了提到的问题之外,菜单本身似乎确实有效。
我根据这里的示例尝试了延迟加载:https://www.angularjswiki.com/material/menu/但是 没有解决问题。
我的HTML模板如下:
<button mat-button [matMenuTriggerFor]="itemMenu">
{{ startMenuItem.description }}
</button>
<mat-menu #itemMenu="matMenu">
<ng-container *ngFor="let childItem of startMenuItem.children">
<ng-container
*ngTemplateOutlet="recursiveMenuTmpl; context: { $implicit: childItem }"
>
</ng-container>
</ng-container>
</mat-menu>
<ng-template #recursiveMenuTmpl let-parentItem>
<ng-container *ngIf="parentItem.children">
<ng-container
*ngTemplateOutlet="menuItem; context: { $implicit: parentItem }"
></ng-container>
</ng-container>
<ng-container *ngIf="!parentItem.children">
<ng-container
*ngTemplateOutlet="singleItem; context: { $implicit: parentItem }"
></ng-container>
</ng-container>
</ng-template>
<ng-template #menuItem let-rootItem>
<button [matMenuTriggerFor]="itemMenu" mat-menu-item>
{{ rootItem.description }}
</button>
<mat-menu #itemMenu="matMenu">
<ng-container *ngFor="let childItem of rootItem.children">
<ng-container
*ngTemplateOutlet="recursiveMenuTmpl; context: { $implicit: childItem }"
>
</ng-container>
</ng-container>
</mat-menu>
</ng-template>
<ng-template #singleItem let-item>
<button *ngIf="item.route" mat-menu-item [routerLink]="item.uri">
{{ item.description }}
</button>
<button *ngIf="!item.route" mat-menu-item (click)="onOpenUrl(item.uri)">
{{ item.description }}
</button>
</ng-template>
和我的组件
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-dynamic-menu',
templateUrl: './dynamic-menu.component.html',
styleUrls: ['./dynamic-menu.component.css'],
})
export class DynamicMenuComponent implements OnInit {
public startMenuItem: MenuItem = {
description: 'Menu',
uri: '',
route: false,
children: [
{
description: 'Welcome',
uri: '',
route: false,
children: [],
},
{
description: 'Other',
uri: '',
route: false,
children: [],
},
{
description: 'Sign on/off',
uri: '',
route: false,
children: [
{
description: 'Login',
uri: '',
route: false,
children: [],
},
{
description: 'Logout',
uri: '',
route: false,
children: [],
},
],
},
],
};
constructor() {}
public onOpenUrl(url: string) {
document.location.href = url;
}
ngOnInit(): void {
}
}
在做了一些额外的研究之后,我发现我的代码中实际上存在一个错误。我的模板中检查菜单项是否有子项的
ngIf
检查子数组是否存在,而不检查数组是否具有实际内容。我解决了这个问题,但不幸的是它没有解决我原来的问题。
对材质菜单行为的进一步分析表明,父子关系的行为并不符合预期。这最终引导我发现了 Angular 已经提出的问题 (https://github.com/angular/angular/issues/14842)。我非常怀疑这个问题导致菜单行为异常。了解这一缺点后,我重新设计了模板,仅使用一个
ng-template
。
材质菜单的问题是缺少子项内容(至少在我的例子中,因为子项位于 ng-template 中)。但是,如果来自
mat-menu
的引用与模板一起传递,则模板中的自定义指令(或者至少这是我修复它的方式)可以执行内容子级并将结果设置在父级 mat-menu
中。在父级 _allItems
中设置 mat-menu
和内容子级并调用 _updateDirectDescendants
就足够了。
不幸的是,模板中指令的依赖注入也存在问题(请参阅:https://github.com/angular/angular/issues/2907),这会阻止在
menu-items
和menu-trigger
中设置父关系。如果所有 parentMenu
中的 menuItems
设置为其相应的父菜单(通过模板上下文传递引用),则 menuItems
的问题也能得到解决。
在菜单触发器上需要设置 _parentMaterialMenu
。最后,对于触发子菜单的 matMenuItems
,应将 triggerSubMenu
设置为 true。
所以总结一下:
menuItems
(例如通过自定义指令)并将其设置在菜单中的_allItems
中。然后拨打菜单上的_updateDirectDescendants
。menuItems
,将 parentMenu
设置为实际的父菜单。menuTrigger
上,_parentMaterialMenu
需要设置为实际的父菜单。menuItems
应将 _triggerSubMenu
设置为 true。就是这样,菜单应该可以正常工作。请注意,此修复不仅仅依赖于 Angular API。它适用于 Angular 材质版本 12,但也可能适用于其他版本。
以下代码是一个工作示例:
动态菜单组件html模板
<button mat-button [matMenuTriggerFor]="itemMenu">{{ startMenuItem.description }}
</button>
<mat-menu #itemMenu="matMenu" #parentMenuRef>
<ng-container *ngTemplateOutlet="recursiveMenuTmpl; context: { $implicit: startMenuItem, parentMenuRef: parentMenuRef }"></ng-container>
</mat-menu>
<ng-template #recursiveMenuTmpl let-rootItem let-parentMenuRef="parentMenuRef">
<ng-container appMenupatch [parentMenu]=parentMenuRef>
<ng-container *ngFor="let childItem of rootItem.children" >
<ng-container *ngIf="hasSubItems(childItem)">
<button #triggerButton="matMenuItem" #trigger="matMenuTrigger" [matMenuTriggerFor]="patch(itemMenu, triggerButton, trigger, parentMenuRef)" mat-menu-item>
{{ childItem.description }}
</button>
<mat-menu #itemMenu="matMenu" #parentMenuRefNew>
<ng-container *ngTemplateOutlet="recursiveMenuTmpl; context: { $implicit: childItem, parentMenuRef: parentMenuRefNew }"></ng-container>
</mat-menu>
</ng-container>
<ng-container *ngIf="!hasSubItems(childItem)">
<button *ngIf="childItem.route" mat-menu-item [routerLink]="childItem.uri">
{{ childItem.description }}
</button>
<button *ngIf="!childItem.route" mat-menu-item (click)="onOpenUrl(childItem.uri)">
{{ childItem.description }}
</button>
</ng-container>
</ng-container>
</ng-container>
</ng-template>
动态菜单组件ts
import { Component, OnInit } from '@angular/core';
import { MatMenu, MatMenuItem, MatMenuPanel, _MatMenuBase } from '@angular/material/menu';
export interface MenuItem {
description: string,
uri: string,
route: boolean,
children: MenuItem[]
}
@Component({
selector: 'app-dynamic-menu',
templateUrl: './dynamic-menu.component.html',
styleUrls: ['./dynamic-menu.component.css'],
})
export class DynamicMenuComponent implements OnInit {
public startMenuItem: MenuItem = {
description: 'Menu',
uri: '',
route: false,
children: []
};
constructor() {}
public onOpenUrl(url: string) {
document.location.href = url;
}
public hasSubItems(item: MenuItem):boolean{
return (Array.isArray(item.children) && item.children.length>0);
}
public patch(item: MatMenu, triggerbutton: MatMenuItem, trigger: any, parentMenu: MatMenuPanel):MatMenu
{
if (parentMenu)
{
triggerbutton._triggersSubmenu=true;
trigger._parentMaterialMenu=parentMenu;
}
return item;
}
ngOnInit(): void {
let menuItem = {
description: 'Canine',
route: true,
children:
[
{
description: 'Dog',
route: false
},
{
description: 'Wolve',
route: false
}
]
} as MenuItem;
this.startMenuItem.children.push(menuItem);
menuItem = {
description: 'Rodent',
route: true,
children:[
{
description: 'Rabbit',
route: false
},
{
description: 'Beaver',
route: false
},
{
description: 'Mouse',
route: false
}
]
} as MenuItem;
this.startMenuItem.children.push(menuItem);
menuItem = {
description: 'Bird of prey',
route: true,
children:
[
{
description: 'Eagle',
route: false
},
{
description: 'Falcon',
route: false
},
{
description: 'Harrier',
route: false
}
]
} as MenuItem;
this.startMenuItem.children.push(menuItem);
}
}
菜单补丁指令
import { Directive, Input, ContentChildren, QueryList } from '@angular/core';
import { MatMenuItem } from '@angular/material/menu';
@Directive({
selector: '[appMenupatch]'
})
export class MenupatchDirective {
@ContentChildren(MatMenuItem, {descendants: true}) _allItems: QueryList<MatMenuItem>;
@Input() parentMenu: any;
constructor() {
}
ngAfterViewChecked(){
this._allItems.forEach(item=>{
item._parentMenu=this.parentMenu;
})
this.parentMenu._allItems=this._allItems;
this.parentMenu._updateDirectDescendants();
}
}
模型声明应包含:
- DynamicMenuComponent
- MenupatchDirective
进口应具有材料模块组件:
- MatMenuModule
- MatButtonModule
也尝试使用模板来解决这个问题,经过几个小时的尝试和网络搜索,我设法通过使用递归组件使其工作。我编辑了别人的旧 Angular 4 解决方案。分享一个在 Angular 13 上测试的工作示例。
https://stackblitz.com/edit/dynamic-nested-hover-mat-menu-nosubitem-support-ifcyo8