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

Angular递归树形菜单:嵌套列表点击仅展开一级,如何实现深层展开?

解决Angular递归树形菜单展开/折叠的层级问题

嘿,我刚碰到过一模一样的问题!你现在遇到的情况,大概率是两个原因导致的:要么是所有节点共用了同一个展开状态变量,要么是点击子节点时事件冒泡触发了父节点的折叠逻辑。咱们一步步来搞定它:

问题根源分析

如果你的代码里用了类似isExpanded这样的单一变量来控制所有节点的展开/折叠,那点击子节点时,不仅子节点的状态会变,父节点的状态也会被意外触发——因为事件会向上冒泡,父节点的点击事件也会执行,最后看起来就是子节点一点击,整个层级都折叠了。

另外,如果没有给每个节点单独维护状态,所有节点会同时展开/折叠,这显然不是你想要的树形菜单效果。

解决方案:给每个节点独立维护状态 + 阻止事件冒泡

第一步:定义带独立状态的菜单数据结构

首先,确保你的菜单节点有自己的展开状态(或者用一个单独的Map来存储状态,不修改原始数据):

// 在sidebar.component.ts里定义菜单节点接口
interface MenuNode {
  id: string; // 每个节点唯一标识,用来跟踪状态
  name: string;
  children?: MenuNode[];
  isExpanded?: boolean; // 可选:直接在节点里存状态,适合前端生成的菜单
}

// 示例菜单数据
menuNodes: MenuNode[] = [
  {
    id: 'folder1',
    name: '一级文件夹',
    isExpanded: false,
    children: [
      {
        id: 'subfolder1',
        name: '二级子文件夹',
        isExpanded: false,
        children: [{ id: 'file1', name: '三级文件' }]
      }
    ]
  }
];

如果你的菜单数据来自后端,不想修改原始数据,可以用Map来存储每个节点的展开状态:

expandedNodes = new Map<string, boolean>();

// 初始化所有节点为折叠状态
this.menuNodes.forEach(node => this.initExpandedState(node));

// 递归初始化子节点
initExpandedState(node: MenuNode): void {
  this.expandedNodes.set(node.id, false);
  node.children?.forEach(child => this.initExpandedState(child));
}

第二步:递归渲染菜单模板(两种方式可选)

方式1:使用ng-template递归(无需子组件)

在sidebar.component.html里用模板递归渲染树形结构,同时处理点击事件:

<!-- 定义递归模板 -->
<ng-template #menuTemplate let-nodes>
  <ul class="menu-list">
    <li *ngFor="let node of nodes" class="menu-item">
      <!-- 点击区域:触发toggle并阻止冒泡 -->
      <div class="node-header" (click)="toggleNode(node, $event)">
        {{ node.name }}
        <!-- 显示展开/折叠图标 -->
        <span *ngIf="node.children?.length" class="toggle-icon">
          {{ getExpandedState(node) ? '▼' : '▶' }}
        </span>
      </div>
      <!-- 递归渲染子节点:仅当节点展开时显示 -->
      <ng-container *ngIf="node.children && getExpandedState(node)">
        <ng-container *ngTemplateOutlet="menuTemplate; context: { $implicit: node.children }"></ng-container>
      </ng-container>
    </li>
  </ul>
</ng-template>

<!-- 渲染根节点 -->
<ng-container *ngTemplateOutlet="menuTemplate; context: { $implicit: menuNodes }"></ng-container>

方式2:使用独立子组件递归

如果菜单结构复杂,可以把每个节点抽成单独的子组件(比如app-menu-node),这样代码更清晰:

子组件menu-node.component.html

<ul>
  <li *ngFor="let node of nodes">
    <div (click)="toggleNode(node, $event)">
      {{ node.name }}
      <span *ngIf="node.children?.length">
        {{ getExpandedState(node) ? '▼' : '▶' }}
      </span>
    </div>
    <app-menu-node *ngIf="node.children && getExpandedState(node)" [nodes]="node.children"></app-menu-node>
  </li>
</ul>

第三步:实现toggle逻辑并阻止事件冒泡

在sidebar.component.ts里编写toggle方法,关键是调用event.stopPropagation()阻止事件向上冒泡:

// 如果是直接在节点里存状态
toggleNode(node: MenuNode, event: Event): void {
  event.stopPropagation(); // 阻止事件冒泡到父节点
  node.isExpanded = !node.isExpanded;
}

getExpandedState(node: MenuNode): boolean {
  return node.isExpanded ?? false;
}

// 如果是用Map存储状态
toggleNode(node: MenuNode, event: Event): void {
  event.stopPropagation();
  const currentState = this.expandedNodes.get(node.id) || false;
  this.expandedNodes.set(node.id, !currentState);
}

getExpandedState(node: MenuNode): boolean {
  return this.expandedNodes.get(node.id) ?? false;
}

关键要点总结

  1. 独立状态:每个节点必须有自己的展开/折叠状态,不能所有节点共用一个变量。
  2. 阻止冒泡:点击子节点时,一定要阻止事件冒泡,否则父节点的点击事件会被触发,导致意外折叠。
  3. 递归渲染:用ng-template或者子组件实现递归,确保树形结构能正确嵌套渲染。

这样修改后,你点击任何层级的节点,只会切换当前节点的展开状态,不会影响父节点啦!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 10:40:57