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

实现两个Angular Material树形视图列表间的拖拽功能求助

实现Angular Material嵌套树形视图跨列表拖拽

核心思路

借助Angular CDK的cdkDropListConnectedTo属性建立两个树形容器的连接,同时在拖拽处理逻辑中区分同容器嵌套拖拽和跨容器拖拽场景,分别处理节点的移动/转移操作。


步骤1:模板改造(两个树形容器)

给每个mat-tree的外层容器添加cdkDropList,通过模板引用变量互相指定connectedTo,确保两个列表能互相接收拖拽元素:

<!-- 第一个树形列表 -->
<div 
  cdkDropList 
  #treeList1="cdkDropList" 
  [cdkDropListConnectedTo]="[treeList2]"
  (cdkDropListDropped)="drop($event, treeData1)"
>
  <mat-tree [dataSource]="dataSource1" [treeControl]="treeControl1" cdkDrag>
    <!-- 基础节点模板 -->
    <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
      <li class="mat-tree-node">
        <div cdkDragHandle>{{node.name}}</div>
      </li>
    </mat-tree-node>
    <!-- 嵌套节点模板 -->
    <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
      <li>
        <div class="mat-tree-node">
          <button mat-icon-button matTreeNodeToggle>
            <mat-icon>{{treeControl1.isExpanded(node) ? 'expand_more' : 'chevron_right'}}</mat-icon>
          </button>
          <div cdkDragHandle>{{node.name}}</div>
        </div>
        <ul [class.mat-tree-invisible]="!treeControl1.isExpanded(node)">
          <ng-container matTreeNodeOutlet></ng-container>
        </ul>
      </li>
    </mat-nested-tree-node>
  </mat-tree>
</div>

<!-- 第二个树形列表 -->
<div 
  cdkDropList 
  #treeList2="cdkDropList" 
  [cdkDropListConnectedTo]="[treeList1]"
  (cdkDropListDropped)="drop($event, treeData2)"
>
  <mat-tree [dataSource]="dataSource2" [treeControl]="treeControl2" cdkDrag>
    <!-- 节点模板和第一个列表完全一致 -->
    <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
      <li class="mat-tree-node">
        <div cdkDragHandle>{{node.name}}</div>
      </li>
    </mat-tree-node>
    <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
      <li>
        <div class="mat-tree-node">
          <button mat-icon-button matTreeNodeToggle>
            <mat-icon>{{treeControl2.isExpanded(node) ? 'expand_more' : 'chevron_right'}}</mat-icon>
          </button>
          <div cdkDragHandle>{{node.name}}</div>
        </div>
        <ul [class.mat-tree-invisible]="!treeControl2.isExpanded(node)">
          <ng-container matTreeNodeOutlet></ng-container>
        </ul>
      </li>
    </mat-nested-tree-node>
  </mat-tree>
</div>

步骤2:组件逻辑处理

定义两个树的数据源和控制器,在drop方法中区分两种拖拽场景:

import { Component } from '@angular/core';
import { NestedTreeControl } from '@angular/cdk/tree';
import { MatTreeNestedDataSource } from '@angular/material/tree';
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

// 保持和原示例一致的节点结构
interface TreeNode {
  name: string;
  children?: TreeNode[];
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  // 第一个树的控制器和数据源
  treeControl1 = new NestedTreeControl<TreeNode>(node => node.children);
  dataSource1 = new MatTreeNestedDataSource<TreeNode>();
  treeData1: TreeNode[] = [
    {
      name: 'Fruit',
      children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Fruit loops'}]
    },
    {name: 'Drink', children: [{name: 'Water'}, {name: 'Soda'}]}
  ];

  // 第二个树的控制器和数据源
  treeControl2 = new NestedTreeControl<TreeNode>(node => node.children);
  dataSource2 = new MatTreeNestedDataSource<TreeNode>();
  treeData2: TreeNode[] = [
    {
      name: 'Vegetables',
      children: [{name: 'Broccoli'}, {name: 'Carrot'}, {name: 'Spinach'}]
    },
    {name: 'Meat', children: [{name: 'Beef'}, {name: 'Chicken'}]}
  ];

  ngOnInit() {
    this.dataSource1.data = this.treeData1;
    this.dataSource2.data = this.treeData2;
  }

  hasChild = (_: number, node: TreeNode) => !!node.children && node.children.length > 0;

  drop(event: CdkDragDrop<TreeNode[]>, targetTreeData: TreeNode[]) {
    // 同容器内拖拽:复用原嵌套拖拽逻辑
    if (event.previousContainer === event.container) {
      // 这里可直接复用你原示例中处理嵌套节点移动的逻辑
      // 示例(根节点层级移动):
      moveItemInArray(targetTreeData, event.previousIndex, event.currentIndex);
    } else {
      // 跨容器拖拽:分两种情况处理
      if (event.currentIndex < targetTreeData.length) {
        // 拖到目标树的根节点层级
        transferArrayItem(
          event.previousContainer.data,
          event.container.data,
          event.previousIndex,
          event.currentIndex
        );
      } else {
        // 拖到目标树的某个子节点下(需结合嵌套逻辑定位父节点)
        const draggedNode = event.previousContainer.data[event.previousIndex];
        // 示例:假设拖到最后一个根节点的子列表中,实际需根据拖拽位置动态定位父节点
        const targetParent = targetTreeData[targetTreeData.length - 1];
        if (!targetParent.children) targetParent.children = [];
        targetParent.children.push(draggedNode);
        // 从原容器移除节点
        event.previousContainer.data.splice(event.previousIndex, 1);
      }
    }
    // 更新数据源引用触发视图刷新
    this.dataSource1.data = [...this.treeData1];
    this.dataSource2.data = [...this.treeData2];
  }
}

关键注意事项

  1. 拖拽手柄:确保cdkDragHandle绑定在节点的可操作区域,避免和树的展开/折叠按钮冲突。
  2. 嵌套节点跨容器拖拽:如果需要支持拖到目标树的子节点下,需结合原示例的节点路径定位逻辑,找到对应的父节点后再添加子节点。
  3. 数据源更新:每次拖拽后必须创建新的数组引用(如[...this.treeData1]),否则Angular的变更检测不会触发树视图更新。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 02:32:44