sortable.js + element-ui制作可拖拽树形表格

2,337 阅读4分钟

最近接到个需求,需要实现Tree-table

  1. 同一父节点下的子节点可以调整排序。

  2. 父节点位置调整,子节点需要跟着一同调整。

目前项目中使用的框架是vue2,UI框架是element-ui,所以tree-table自然而然的想到了element-ui中的table组件。

拖拽功能的实现基于自己使用过的有vuedraggable,sortable.js两种,本案例中选择使用了sortable.js

可拖拽tree-table的必要条件都已敲定

实现思路

拖拽DOM,调换顺序

那么选定了 vue2 + element-ui + sortable.js,又是如何确定这些结合起来就能实现需求的呢?

首先,可拖拽的要点就是在于确定可拖拽的dom项可存放拖拽项容器

我观察了一下element的tree-tabble渲染后的dom结构发现,所有的行都以tr实现,并且存在于当中,其次所有的节点也是扁平化的tr平铺,子节点的tr会跟在父节点的tr之后,并会有额外的样式名el-table__row--level-[层级:如0、1、2] , 不存在嵌套的table。

这么一来就很容易确定了容器 ,可拖拽项为tr[class=.el-table-row]

接着我们开始代码实现吧

  1. .vue中的template模板上定义一个el-table, 注意需要指定row-key

1.1 指定row-key才能成为treeTable

  1. mock一份treeTable的Data如下
{
    treeTableData: [        
     {            
      id: 1,            
      name: 'a',            
      age: 11,            
      desc: '喝喝',            
      children: 
        [                
         {                    
          id: 2,                    
          name: 'b',                    
          age: 15,                    
          desc: '嘻嘻'                
         }            
        ]        
     },        
    {            
     id: 3,            
     name: 'c',            
     age: 18,            
     desc: '哈哈',            
     children: 
      [                
       {                    
        id: 4,                    
        name: 'd',                    
        age: 25,                    
        desc: '嘿嘿'                
       }            
     ]        
    },              
  ]
}
  1. 定义初始化Sortable拖拽容器、可拖拽项的方法
  initSortable() {        
    // 获取容器元素        
    const el = this.$refs.dragTable.$el.querySelectorAll('.el-table__body-wrapper > table > tbody')[0];
    if(!el) return;        
    this.sortableObj = new Sortable(el, 
    {
        group: 'dragName',        
        draggable: '.el-table__row',            
        onEnd: () => {                
            console.log(123)            
        }        
    })    
  }

完整如下:

<!-- 可拖拽表格 --><template>
  <div>
    <p>拖拽表格</p>
    <el-table ref="dragTable" :data="treeTableData" border row-key="id">
      <el-table-column prop="name" label="名字"></el-table-column>
      <el-table-column prop="age" label="年龄"></el-table-column>
      <el-table-column prop="desc" label="描述"></el-table-column>
    </el-table>
  </div>
</template><script>
import Sortable from "sortablejs";
export default {
  data() {
    return {
      treeTableData: [
        {
          id: 1,
          name: "a",
          age: 11,
          desc: "喝喝",
          children: [{ id: 2, name: "b", age: 15, desc: "嘻嘻" }],
        },
        {
          id: 3,
          name: "c",
          age: 18,
          desc: "哈哈",
          children: [{ id: 4, name: "d", age: 25, desc: "嘿嘿" }],
        },
      ],
      sortableObj: null,
    };
  },
  mounted() {
    this.initSortable();
  },
  methods: {
    initSortable() {
      const el = this.$refs.dragTable.$el.querySelectorAll(
        ".el-table__body-wrapper > table > tbody"
      )[0];

      if (!el) return;
      this.sortableObj = new Sortable(el, {
        group: "dragName",
        draggable: ".el-table__row",
        onEnd: () => {
          console.log(123);
        },
      });
    },
  },
};
</script><style lang="scss" scoped></style>

看到这里,OK,你可以看到一个可以随便你拖拽的表格了,但是。。。

咋回事, 父行位置变动后,对应的子行怎么没有跟着下去呢?

因为sortable只处理DOM元素的顺序变化,要想让子元素变化,最根本的是要将treeTable的数据源也按照拖拽后的顺序处理

同步数据源

思路:

实现以上思路的关键在于根据dom节点中的顺序生成最新的treeData的属性结构数据:

无论是父行、子行,渲染成table中的tr都是在同一层级的tbody下,

  1. 如何根据平铺的数据类型转换成treeData接收的带有children的树形结构的数据类型呢?

先再次观察treeData的dom结构图

得出结论

虽然数据结构是扁平化的,但是还是有规律的,规律就是所有的子节点tr都是按照树形结构中的顺序跟在对应的父节点tr后

那么我们只需要先将treeData按照treeTable中的顺序扁平化处理后使用一个变量flatTreeData来存放,对应上每个tr的dom。发生拖拽后将flatTreeData中的数据顺序按照dom顺序对应调整,再将flatTreeData变成treeData树形结构的数据,就可以保证视图与数据一致了

  1. 当拖拽父节点的时候,如何将父节点下的所有子孙节点的位置同步更改呢?

当有了flatTreeData后,当父节点发生了改变后,判断其下一个节点是否与它是同一层级的节点,如果不是的话必然是当前父节点的子节点,移至当前父节点的新位置后方,一直遍历到相同层级的节点为止。

我们当前的数据结构是无法判断层级的,所以我们会在每个节点上加一个字段level用于判断层级

  1. 不同父节点下的同一层级子节点如何限制?

同样,我们只需要给节点字段中加一个parentId即可

效果

踩坑经验

到这里你以为好了吗?并没有,还有个问题,看图

带有子节点的父节点从下往上拉,子节点dom会更新位置,自动往上走,

带有子节点的父节点从上往下拉,子节点dom会不更新位置。

两种情况对应调整后的treeData是一模一样的,经过断点查看,应该是Vue在PatchNode的时候没有更新,但是我还没有完全定位清楚原因,如果后期定位清楚了再来补充。有大佬知道为什么也可以在评论区指点一下。

目前解决办法就是

DOM更新: 先将treeData清空,然后通过$nextTick给treeData赋顺序调整后的值。

保留展开状态:  element-ui的Tree组件存在一个expand-row-keys,配合expand-change记录处于展开状态的节点。 (注意:  row-key对应的值类型需要为string,如id如果为int类型则expand-row-keys无效,id为string才行)

效果

demo地址

github.com/HHH1994/tre…

至此,本篇文章内容部分就结束了,第一次发文章,如果觉得还可以点个赞吧,如果有问题可以评论区交流,轻喷~

参考资料:

Sortablejs官网:www.sortablejs.com/

element-ui组件官网:element.eleme.cn/#/zh-CN/com…