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

Angular Material嵌套树是否支持拖拽?如何实现拖拽功能?

Angular Material 嵌套树拖拽功能说明

核心结论

Angular Material 嵌套树(Nested Tree)无官方原生支持的拖拽方案,你此前了解的信息准确:官方更推荐使用扁平树(Flat Tree)实现拖拽,因为扁平树的DOM结构为单层级列表,仅通过内边距区分嵌套层级,和CDK拖拽模块的适配成本极低,性能更优。

嵌套树并非完全无法实现拖拽,但是需要自行编写大量自定义逻辑处理嵌套DOM结构带来的定位、层级识别、数据源更新问题,开发和维护成本远高于使用扁平树方案。


嵌套树拖拽实现示例(基于你提供的代码改造)

前置依赖

首先确保你的Angular模块中已导入DragDropModule:

import { DragDropModule } from '@angular/cdk/drag-drop';

@NgModule({
  imports: [
    // 其他已导入的模块
    DragDropModule,
    MatTreeModule,
    MatIconModule
  ]
})

模板改造

需要在原有嵌套树模板基础上增加拖拽相关的指令和事件处理:

<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" cdkDropList (cdkDropListDropped)="onDrop($event)">
  <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild" cdkDrag [cdkDragData]="node">
    <!-- 拖拽预览自定义 -->
    <div *cdkDragPreview class="drag-preview">
      {{node.sectionTitle}}
    </div>

    <div>
      <div class="mat-tree-node" matTreeNodeToggle (contextmenu)="selectednode = node;onRightClick($event);"
      [ngClass]="{ 'background-highlight':selectednode === node  }">
        <mat-icon class="folder-icon-spacing" fontSet="far"
          [fontIcon]="treeControl.isExpanded(node) ? 'fa-folder-open' : 'fa-folder'">
        </mat-icon>
        <mat-icon class="mat-icon-rtl-mirror chevron-color">
          {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
        </mat-icon>
        <div class="node-format item-font">
          {{node.sectionTitle}}</div>
        <div class="align-setproperties item-font" (click)="showProperties(node)">
          <span *ngIf="node.sectionOrder !== 'Select one';else showprop">
            <span
              *ngIf="node.randomCount === 0 || node.randomCount === '' || node.randomCount == null || node.hasTitleError ||  node.hasRandomError ; else validCount">
              <span
                *ngIf="node.hasTitleError && !node.hasRandomError; else invalid">
                {{node.sectionOrder}} - {{node.randomCount}} of {{node.totalCount}}
               </span>
              <ng-template #invalid>
                <span>
                  {{node.sectionOrder}} - ? of {{node.totalCount}}
                </span>
              </ng-template>
            </span>
            <ng-template #validCount>
              <span class="validText">{{node.sectionOrder}} - {{node.randomCount}} of {{node.totalCount}}
              </span>
            </ng-template>
          </span>
          <ng-template #showprop>Set properties</ng-template>
        </div>
        <div style="visibility: hidden; position: fixed" [style.left]="contextMenuPosition.x"
          [style.top]="contextMenuPosition.y" [matMenuTriggerFor]="contextMenu">
        </div>
        <mat-menu #contextMenu="matMenu">
          <button mat-menu-item (click)="add(selectednode)">Add</button>
          <button mat-menu-item *ngIf="selectednode && selectednode.sectionIdent === 0 ? false: true"
            (click)="removeNode(selectednode)">Remove</button>
        </mat-menu>
      </div>
    </div>

    <div [class.example-tree-invisible]="!treeControl.isExpanded(node)" role="group" cdkDropList [cdkDropListData]="node.children" (cdkDropListDropped)="onDrop($event, node)">
      <ng-container matTreeNodeOutlet>
      </ng-container>
    </div>
    <div *ngFor="let sibling of node.items" style="display:block;min-height: 10px;padding-left: 96px;">
      {{sibling}}
    </div>
  </mat-nested-tree-node>

  <!-- 叶子节点定义,你原有代码缺失部分,需要补充 -->
  <mat-tree-node *matTreeNodeDef="let node" cdkDrag [cdkDragData]="node">
    <div class="mat-tree-node">
      <!-- 叶子节点内容和上面父节点内容保持一致即可 -->
    </div>
  </mat-tree-node>
</mat-tree>

TS逻辑补充

需要实现拖拽完成后的数据源更新逻辑:

import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

// 拖拽完成事件处理
onDrop(event: CdkDragDrop<any[]>, parentNode?: any) {
  // 同一父级下拖拽排序
  if (event.previousContainer === event.container) {
    moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
  } else {
    // 跨父级拖拽,转移节点
    transferArrayItem(
      event.previousContainer.data,
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
  // 刷新数据源
  this.dataSource.data = [...this.dataSource.data];
}

// 原有hasChild方法保持不变
hasChild = (_: number, node: any) => !!node.children && node.children.length > 0;

注意事项

  • 以上仅为最小实现样例,你还需要自行处理边界场景:比如限制根节点不能拖入子级、折叠状态下的节点放置判定、拖拽位置提示(放在目标上方/下方/作为子节点)等
  • 嵌套树拖拽的性能远低于扁平树,当节点数量较多时会出现明显卡顿,若非业务必须使用嵌套树,优先切换为扁平树实现拖拽功能

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 05:15:03