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

寻求Angular独立下拉组件:替代Material的mat-menu方案

解决方案:独立Angular下拉菜单组件实现

一、移除mat-menu依赖,实现自定义触发逻辑

完全替代mat-menu的hover/点击触发逻辑,无需依赖任何第三方框架:

  • 点击触发:绑定(click)事件切换菜单显示状态,通过HostListener监听文档点击事件,实现点击外部关闭菜单
  • Hover触发:绑定(mouseenter)/(mouseleave)事件控制显隐,处理多级菜单的hover冒泡问题,避免子菜单误关闭

二、多方向展开实现

通过动态绑定CSS类控制菜单定位,支持上下左右四个方向:

  • 定义@Input() direction参数,默认值为bottom
  • 针对不同方向编写定位样式,也可拓展自动计算视口空间的逻辑(可选优化)

三、多级菜单结构处理

基于传入的Pages数组(@Input类型)递归渲染菜单:

  • 每个菜单项若包含children属性,则渲染子菜单
  • 子菜单默认方向设为right,支持自定义传入方向
  • 使用ng-template实现递归渲染,简化层级结构

完整核心代码示例

组件类(menu.component.ts)

import { Component, Input, HostListener } from '@angular/core';

export interface MenuItem {
  label: string;
  route?: string;
  children?: MenuItem[];
  direction?: 'top' | 'bottom' | 'left' | 'right';
  isSubMenuOpen?: boolean;
}

@Component({
  selector: 'app-custom-menu',
  templateUrl: './custom-menu.component.html',
  styleUrls: ['./custom-menu.component.css']
})
export class CustomMenuComponent {
  @Input() pages: MenuItem[] = [];
  @Input() triggerType: 'click' | 'hover' = 'click';
  @Input() direction: 'top' | 'bottom' | 'left' | 'right' = 'bottom';
  isMenuOpen = false;

  @HostListener('document:click', ['$event'])
  handleDocumentClick(event: MouseEvent) {
    const target = event.target as HTMLElement;
    if (!target.closest('.custom-menu-trigger') && !target.closest('.custom-menu')) {
      this.closeAllMenus();
    }
  }

  toggleMenu() {
    if (this.triggerType === 'click') {
      this.isMenuOpen = !this.isMenuOpen;
    }
  }

  openSubMenu(item: MenuItem) {
    if (this.triggerType === 'hover') {
      this.closeAllSubMenus();
      item.isSubMenuOpen = true;
    }
  }

  closeSubMenu(item: MenuItem) {
    if (this.triggerType === 'hover') {
      item.isSubMenuOpen = false;
    }
  }

  handleItemClick(item: MenuItem) {
    if (!item.children) {
      // 此处添加菜单项点击逻辑(如路由跳转)
      this.closeAllMenus();
    } else {
      item.isSubMenuOpen = !item.isSubMenuOpen;
    }
  }

  private closeAllMenus() {
    this.isMenuOpen = false;
    this.closeAllSubMenus();
  }

  private closeAllSubMenus() {
    const closeChildren = (items: MenuItem[]) => {
      items.forEach(item => {
        item.isSubMenuOpen = false;
        if (item.children) closeChildren(item.children);
      });
    };
    closeChildren(this.pages);
  }

  getSubMenuClass(direction?: string) {
    const dir = direction || 'right';
    return `menu-${dir}`;
  }
}

模板(custom-menu.component.html)

<button class="custom-menu-trigger" 
        (click)="toggleMenu()" 
        (mouseenter)="triggerType === 'hover' && (isMenuOpen = true)" 
        (mouseleave)="triggerType === 'hover' && (isMenuOpen = false)">
  打开菜单
</button>

<div *ngIf="isMenuOpen" class="custom-menu" [ngClass]="`menu-${direction}`">
  <ng-template #menuItem let-items>
    <ul class="menu-list">
      <li *ngFor="let item of items" class="menu-item">
        <div class="item-content" 
             (click)="handleItemClick(item)" 
             (mouseenter)="openSubMenu(item)" 
             (mouseleave)="closeSubMenu(item)">
          <span>{{item.label}}</span>
          <span *ngIf="item.children" class="menu-arrow">›</span>
        </div>
        <div *ngIf="item.children && item.isSubMenuOpen" 
             class="custom-submenu" 
             [ngClass]="getSubMenuClass(item.direction)">
          <ng-container *ngTemplateOutlet="menuItem; context: { $implicit: item.children }"></ng-container>
        </div>
      </li>
    </ul>
  </ng-template>

  <ng-container *ngTemplateOutlet="menuItem; context: { $implicit: pages }"></ng-container>
</div>

样式(custom-menu.component.css)

.custom-menu-trigger {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background: #f1f1f1;
  cursor: pointer;
}

.custom-menu, .custom-submenu {
  position: absolute;
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  min-width: 180px;
  z-index: 1000;
}

.custom-submenu {
  z-index: 1001;
}

.menu-top {
  bottom: 100%;
  left: 0;
  margin-bottom: 8px;
}

.menu-bottom {
  top: 100%;
  left: 0;
  margin-top: 8px;
}

.menu-left {
  right: 100%;
  top: 0;
  margin-right: 8px;
}

.menu-right {
  left: 100%;
  top: 0;
  margin-left: 8px;
}

.menu-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.menu-item {
  position: relative;
}

.item-content {
  padding: 8px 16px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.item-content:hover {
  background: #f5f5f5;
}

.menu-arrow {
  font-size: 12px;
  color: #999;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 23:07:26