最近接到个需求,需要实现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]
接着我们开始代码实现吧
- .vue中的template模板上定义一个el-table, 注意需要指定row-key
1.1 指定row-key才能成为treeTable
- 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: '嘿嘿'
}
]
},
]
}
- 定义初始化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下,
- 如何根据平铺的数据类型转换成treeData接收的带有children的树形结构的数据类型呢?
先再次观察treeData的dom结构图
得出结论
虽然数据结构是扁平化的,但是还是有规律的,规律就是所有的子节点tr都是按照树形结构中的顺序跟在对应的父节点tr后
那么我们只需要先将treeData按照treeTable中的顺序扁平化处理后使用一个变量flatTreeData来存放,对应上每个tr的dom。发生拖拽后将flatTreeData中的数据顺序按照dom顺序对应调整,再将flatTreeData变成treeData树形结构的数据,就可以保证视图与数据一致了
- 当拖拽父节点的时候,如何将父节点下的所有子孙节点的位置同步更改呢?
当有了flatTreeData后,当父节点发生了改变后,判断其下一个节点是否与它是同一层级的节点,如果不是的话必然是当前父节点的子节点,移至当前父节点的新位置后方,一直遍历到相同层级的节点为止。
我们当前的数据结构是无法判断层级的,所以我们会在每个节点上加一个字段level用于判断层级
- 不同父节点下的同一层级子节点如何限制?
同样,我们只需要给节点字段中加一个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地址
至此,本篇文章内容部分就结束了,第一次发文章,如果觉得还可以点个赞吧,如果有问题可以评论区交流,轻喷~
参考资料:
Sortablejs官网:www.sortablejs.com/
element-ui组件官网:element.eleme.cn/#/zh-CN/com…