共计 11219 个字符,预计需要花费 29 分钟才能阅读完成。
ant-design-vue 中 tree 增删改
1. 应用背景
新我的项目中应用了 ant-design-vue
组件库. 该组件库齐全根基数据双向绑定的模式实现. 只有表单组件提供大量的办法. 所以, 在应用 ant-design-vue
时, 肯定要从扭转数据的角度去切换 UI 显示成果. 然而, 在树形控件 a-tree
的应用上, 单从数据驱动下来思考, 感体验成果切实不好.
2. 以后痛点
通过浏览官网帮忙文档, 针对树形控件数据绑定. 须要将数据结构成一个蕴含 children,title,key
属性的大对象. 这样一个对象, 要么通过后端结构好这样的 json 对象, 要么就是后端给前端一个 json 数组, 前端依据上下级关系构建这么一个树形对象. 数据绑定好, 就能够胜利的渲染成咱们想要的 UI 成果了. 可痛点在哪里呢?
- 树形加载胜利后, 我要向以后的树形增加一个同级以及上级节点该如何操作(增)
- 树形加载胜利后, 我要批改任意一个树形节点该如何操作(改)
- 树形加载胜利后, 我要删除一个树形节点该如何操作(删)
以上操作, 都要求不从新加载树形控件条件下实现. 通过测试整顿出了三个可行计划
- 数据驱动
- 作用域插槽
- 节点事件
3. 数据驱动实现树形节点增删改
咱们能够在帮忙文档中找到名为 selectedKeys(.sync)
属性,sync
示意该属性反对双向操作. 然而, 这里仅仅获取的是一个 key
值, 并不是须要的绑定对象. 所以, 须要通过这 key 值找到这个对象.须要找这个对象就相当恶心了
- 如果后端返回是构建好的数据, 须要遍历这个树形数据中找到和这个 key 值对应的对象. 我能想到的就是通过顶层节点递归查找. 可是控件都渲染实现了, 都晓得每个节点的数据. 我为什要从新查找一遍呢???
- 如果后端返回的仅仅是一个数组, 这个方才有提到须要从新构建这部分数据为对象. 这样查找这个对象又分两种状况
a. 如果列表数据和构建后树形对象采纳克隆的形式, 也就是列表中对象的地址和树形中雷同 key 值对象的地址不同. 须要通过办法 1 遍历从新结构后的树形数据
b. 如果列表数据中的对象和构建后对应的节点是雷同的对象地址. 能够间接查找这个列表数据失去对应的对象.
所以, 恶心的中央就在于 构建好一个树, 我又得遍历这个树查找某个节点 , 或者采纳计划 b 这种 空间换工夫 的做法
这里咱们假如数据, 曾经是构建成树形的数据格式. 要实现数据驱动的首要任务须要实现两个外围办法
- 依据以后节点 key 值查找节点对象
getTreeDataByKey
- 依据以后节点 key 值查找父级节点 children 汇合
getTreeParentChilds
两个办法代码别离如下
// author:herbert date:20201024 qq:464884492 | |
// 依据 key 获取与之相等的数据对象 | |
getTreeDataByKey(childs = [], findKey) { | |
let finditem = null; | |
for (let i = 0, len = childs.length; i < len; i++) {let item = childs[i] | |
if (item.key !== findKey && item.children && item.children.length > 0) {finditem = this.getTreeDataByKey(item.children, findKey) | |
} | |
if (item.key == findKey) {finditem = item} | |
if (finditem != null) {break} | |
} | |
return finditem | |
}, | |
// author:herbert date:20201024 qq:464884492 | |
// 依据 key 获取父级节点 children 数组 | |
getTreeParentChilds(childs = [], findKey) {let parentChilds = [] | |
for (let i = 0, len = childs.length; i < len; i++) {let item = childs[i] | |
if (item.key !== findKey && item.children && item.children.length > 0) {parentChilds = this.getTreeParentChilds(item.children, findKey) | |
} | |
if (item.key == findKey) {parentChilds = childs} | |
if (parentChilds.length > 0) {break} | |
} | |
return parentChilds | |
}, |
3.1 增加同级节点
增加同级节点, 须要把新减少的数据, 增加到以后选中节点的父级的 children
数组中. 所以, 增加节点的难点在如何找到以后选中节点的绑定对象的父级对象. 页面代码如下
<!-- author:herbert date:20201030 qq:464884492--> | |
<a-card style="width: 450px;height:550px;float: left;"> | |
<div slot="title"> | |
<h2> 树形操作(纯数据驱动)<span style="color:blue">@herbert</span></h2> | |
<div> | |
<a-button @click="dataDriveAddSame"> 增加同级 </a-button> | |
<a-divider type="vertical" /> | |
<a-button @click="dataDriveAddSub"> 增加上级 </a-button> | |
<a-divider type="vertical" /> | |
<a-button @click="dataDriveModify"> 批改 </a-button> | |
<a-divider type="vertical" /> | |
<a-button @click="dataDriveDelete"> 删除 </a-button> | |
</div> | |
</div> | |
<a-tree :tree-data="treeData" :defaultExpandAll="true" | |
:selectedKeys.sync="selectKeys" showLine /> | |
<img src="./assets/gamelogo.png" width="100%" style="margin-top:20px" /> | |
</a-card> |
从页面代码中能够看出, 再树上绑定了两个属性 tree-data
,selectedKeys
, 这里咱们就能够通过selectedKeys
绑定值, 获取到树形以后抉择的 key
值. 而后应用办法 getTreeParentChilds
就能够实现同级增加. 所以, 对用的 dataDriveAddSame
代码实现如下
// author:herbert date:20201030 qq:464884492 | |
dataDriveAddSame() {let parentChilds = this.getTreeParentChilds(this.treeData, this.selectKeys[0]) | |
parentChilds.forEach(item => console.log(item.title)); | |
parentChilds.push({ | |
title: '地心侠士, 会玩就停不下来', | |
key: new Date().getTime() | |
}) | |
}, |
3.2 增加上级
有了上边的根底, 增加上级就很简略了. 惟一须要留神的中央就是 获取到的对象 children 属性可能不存在, 此时咱们须要 $set 形式增加属性 dataDriveAddSub
代码实现如下
// author:herbert date:20201030 qq:464884492 | |
dataDriveAddSub() {let selectItem = this.getTreeDataByKey(this.treeData, this.selectKeys[0]) | |
if (!selectItem.children) {this.$set(selectItem, "children", []) | |
} | |
selectItem.children.push({ | |
title: 地心侠士, 值得你来玩, | |
key: new Date().getTime() | |
}) | |
this.$forceUpdate()}, |
3.3 批改节点
能获取到绑定对象, 批改节点值也变得简略了, 同增加上级一样应用 getTreeDataByKey
获取以后对象, 而后间接批改值就是了.dataDriveModify
代码实现如下
// author:herbert date:20201030 qq:464884492 | |
dataDriveModify() {let selectItem = this.getTreeDataByKey(this.treeData, this.selectKeys[0]) | |
selectItem.title = '扫码下方二维码, 开始地心探险之旅' | |
}, |
3.4 删除节点
删除和增加同级一样, 须要找到父级节点 children
数组, 曾经以后对象在父级数组中对应的索引.dataDriveDelete
代码实现如下
// author:herbert date:20201030 qq:464884492 | |
dataDriveDelete() {let parentChilds = this.getTreeParentChilds(this.treeData, this.selectKeys[0]) | |
let delIndex = parentChilds.findIndex(item => item.key == this.selectKeys[0]) | |
parentChilds.splice(delIndex, 1) | |
}, |
4. 通过插槽形式树形节点增删改
在 ant-tree
的 api 中, 树形节点属性 title
类型能够是字符串, 也能够是插槽[string|slot|slot-scope
], 我么这里须要拿到操作对象, 这里应用作用域插槽, 对应的页面代码如下
<!-- author:herbert date:20201030 qq:464884492--> | |
<a-card style="width: 450px;height:550px;float: left;"> | |
<div slot="title"> | |
<h2> 树形操作(采纳作用域插槽)</h2> | |
<div> | |
采纳作用域插槽, 操作按钮对立搁置到树上 <span style="color:blue">@小院不小 </span> | |
</div> | |
</div> | |
<a-tree ref="tree1" :tree-data="treeData1" :defaultExpandAll="true" :selectedKeys.sync="selectKeys1" showLine blockNode> | |
<template v-slot:title="nodeData"> | |
<span>{{nodeData.title}}</span> | |
<a-button-group style="float:right"> | |
<a-button size="small" @click="slotAddSame(nodeData)" icon="plus-circle" title="增加同级"></a-button> | |
<a-button size="small" @click="slotAddSub(nodeData)" icon="share-alt" title="增加上级"></a-button> | |
<a-button size="small" @click="slotModify(nodeData)" icon="form" title="批改"></a-button> | |
<a-button size="small" @click="slotDelete(nodeData)" icon="close-circle" title="删除"></a-button> | |
</a-button-group> | |
</template> | |
</a-tree> | |
<img src="./assets/gamelogo.png" width="100%" style="margin-top:20px" /> | |
</a-card> |
4.1 增加同级
采纳插槽的形式拿到对象, 其实是以后节点对应的属性值, 并且是一个浅复制的正本. 在源码 vc-tree\\src\\TreeNode.jsx
中的 renderSelector
能够找到如下一段代码
const currentTitle = title; | |
let $title = currentTitle ? (<span class={`${prefixCls}-title`}> | |
{typeof currentTitle === 'function' | |
? currentTitle({...this.$props, ...this.$props.dataRef}, h) | |
: currentTitle} | |
</span> | |
) : (<span class={`${prefixCls}-title`}>{defaultTitle}</span> | |
); |
从这段代码, 能够看到一个 dataRef. 然而在官网的帮忙文档中齐全没有这个属性的介绍. 不晓得者算不算给违心看源码的同学的一种福利. 不论从代码层面, 还是调试后果看. 通过作用域失去的对象, 没有父级属性所以 不能实现同级增加 .slotAddSame
代码如下
// author:herbert date:20201030 qq:464884492 | |
slotAddSame(nodeItem) {console.log(nodeItem) | |
this.$warn({content: "采纳插槽形式, 找不到父级对象, 增加失败! 不要想了, 去玩地心侠士吧"}) | |
}, |
4.2 增加上级
尽管失去了对象, 然而只是一个正本. 所以设置 children
也是没用的!!
// author:herbert date:20201030 qq:464884492 | |
slotAddSub(nodeItem) {if (!nodeItem.children) {console.log('其实这个判断没有用, 这里仅仅是一个正本') | |
this.$set(nodeItem, "children", []) | |
} | |
nodeItem.children.push({ | |
title: this.addSubTitle, | |
key: new Date().getTime(), | |
scopedSlots: {title: 'title'}, | |
children: []}) | |
}, |
4.3 批改节点
批改一样也不能实现, 不过上边有提到dataRef
, 这里简略应用下, 能够实现批改 title 值.
// author:herbert date:20201030 qq:464884492 | |
slotModify(nodeItem) {console.log(nodeItem) | |
console.log('nodeItem 仅仅时渲染 Treenode 属性的一个浅复制的正本, 间接批改 Title 没有用') | |
nodeItem.title = '这里设置是没有用的, 去玩游戏劳动一会吧' | |
// 这里能够借助 dataRef 更新 | |
nodeItem.dataRef.title = nodeItem.title | |
}, |
4.4 删除节点
很显著, 删除也是不能够的.
// author:herbert date:20201030 qq:464884492 | |
slodDelete(nodeItem) {console.log(nodeItem) | |
this.$warn({content: "采纳插槽形式, 找不到父级对象, 删除失败! 很显著, 还是去玩地心侠士吧"}) | |
delete nodeItem.dataRef | |
}, |
5. 树形事件联合 dataRef 实现
上边通过插槽形式, 仅仅实现了批改性能. 特地悲观有没有. 不过从设计的角度去思考, 给你对象仅仅是帮忙你做自定义渲染, 就好多了. 接续读官网 Api 找到 事件 其中的 select
事件提供的值, 又给了咱们很大的施展空间. 到底有多大呢, 咱们去源码看看. 首先咱们找到触发 select
事件代码在 components\\vc-tree\\src\\TreeNode.jsx
文件中, 具体代码如下
onSelect(e) {if (this.isDisabled()) return; | |
const {vcTree: { onNodeSelect}, | |
} = this; | |
e.preventDefault(); | |
onNodeSelect(e, this); | |
}, |
从代码中能够看到 TreeNode
onSelect 其实是调用Tree
中的 onNodeSelected 办法, 咱们到 components\\vc-tree\\src\\Tree.jsx
找到对应的代码如下
onNodeSelect(e, treeNode) {let { _selectedKeys: selectedKeys} = this.$data; | |
const {_keyEntities: keyEntities} = this.$data; | |
const {multiple} = this.$props; | |
const {selected, eventKey} = getOptionProps(treeNode); | |
const targetSelected = !selected; | |
// Update selected keys | |
if (!targetSelected) {selectedKeys = arrDel(selectedKeys, eventKey); | |
} else if (!multiple) {selectedKeys = [eventKey]; | |
} else {selectedKeys = arrAdd(selectedKeys, eventKey); | |
} | |
// [Legacy] Not found related usage in doc or upper libs | |
const selectedNodes = selectedKeys | |
.map(key => {const entity = keyEntities.get(key); | |
if (!entity) return null; | |
return entity.node; | |
}) | |
.filter(node => node); | |
this.setUncontrolledState({_selectedKeys: selectedKeys}); | |
const eventObj = { | |
event: 'select', | |
selected: targetSelected, | |
node: treeNode, | |
selectedNodes, | |
nativeEvent: e, | |
}; | |
this.__emit('update:selectedKeys', selectedKeys); | |
this.__emit('select', selectedKeys, eventObj); | |
}, |
联合两个办法, 从 Tree 节点 eventObj 对象中能够晓得组件 select
不仅把 Tree 节点渲染 TreeNode 缓存数据 selectedNodes
以及对应实实在在的 TreeNode 节点node
, 都提供给了调用方. 有了这个 node 属性, 咱们就能够拿到对应节点的上下级关系
接下来咱们说说这个再帮忙文档上没有呈现的 dataRef
是个什么鬼.
找到文件 components\\tree\\Tree.jsx
在对应的 render
函数中咱们能够晓得 Tree 须要向 vc-tree 组件传递一个 treeData
属性, 咱们最终应用的传递节点数据也是这个属性名. 两段要害代码如下
render(){ | |
... | |
let treeData = props.treeData || treeNodes; | |
if (treeData) {treeData = this.updateTreeData(treeData); | |
} | |
... | |
if (treeData) {vcTreeProps.props.treeData = treeData;} | |
return <VcTree {...vcTreeProps} />; | |
} |
从上边代码能够看到, 组件底层调用办法 updateTreeData
对咱们传入的数据做了解决, 这个办法要害代码如下
updateTreeData(treeData) {const { $slots, $scopedSlots} = this; | |
const defaultFields = {children: 'children', title: 'title', key: 'key'}; | |
const replaceFields = {...defaultFields, ...this.$props.replaceFields}; | |
return treeData.map(item => {const key = item[replaceFields.key]; | |
const children = item[replaceFields.children]; | |
const {on = {}, slots = {}, scopedSlots = {}, class: cls, style, ...restProps } = item; | |
const treeNodeProps = { | |
...restProps, | |
icon: $scopedSlots[scopedSlots.icon] || $slots[slots.icon] || restProps.icon, | |
switcherIcon: | |
$scopedSlots[scopedSlots.switcherIcon] || | |
$slots[slots.switcherIcon] || | |
restProps.switcherIcon, | |
title: | |
$scopedSlots[scopedSlots.title] || | |
$slots[slots.title] || | |
restProps[replaceFields.title], | |
dataRef: item, | |
on, | |
key, | |
class: cls, | |
style, | |
}; | |
if (children) { | |
// herbert 20200928 增加属性只能操作叶子节点 | |
if (this.onlyLeafEnable === true) {treeNodeProps.disabled = true;} | |
return {...treeNodeProps, children: this.updateTreeData(children) }; | |
} | |
return treeNodeProps; | |
}); | |
}, | |
} |
从这个办法中咱们看到, 在 treeNodeProps
属性找到了 dataRef
属性, 它的值就是咱们传入 treeData
中的数据项, 所以这个属性是反对 双向绑定 的哦. 这个 treeNodeProps
最终会渲染到components\\vc-tree\\src\\TreeNode.jsx
, 组件中去.
弄清楚这两个知识点后, 咱们要做的操作就变得简略了. 事件驱动页面代码如下
<!-- author:herbert date:20201101 qq:464884492 --> | |
<a-card style="width: 450px;height:550px;float: left;"> | |
<div slot="title"> | |
<h2> 树形事件(联合 dataRef)<span style="color:blue">@464884492</span></h2> | |
<div> | |
<a-button @click="eventAddSame"> 增加同级 </a-button> | |
<a-divider type="vertical" /> | |
<a-button @click="eventAddSub"> 增加上级 </a-button> | |
<a-divider type="vertical" /> | |
<a-button @click="eventModify"> 批改 </a-button> | |
<a-divider type="vertical" /> | |
<a-button @click="eventDelete"> 删除 </a-button> | |
</div> | |
</div> | |
<a-tree :tree-data="treeData2" @select="onEventTreeNodeSelected" :defaultExpandAll="true" :selectedKeys.sync="selectKeys2" showLine /> | |
<img src="./assets/gamelogo.png" width="100%" style="margin-top:20px" /> | |
</a-card> |
既然是通过事件驱动, 咱们首先得注册对应得事件, 代码如下
// author:herbert date:20201101 qq:464884492 | |
onEventTreeNodeSelected(seleteKeys, e) {if (e.selected) { | |
this.eventSelectedNode = e.node | |
return | |
} | |
this.eventSelectedNode = null | |
}, |
在事件中, 咱们保留以后抉择 TreeNode 不便后续的减少批改删除
5.1 增加同级
利用 vue 虚构 dom, 找到父级
// author:herbert date:20201101 qq:464884492 | |
eventAddSame() { | |
// 查找父级 | |
let dataRef = this.eventSelectedNode.$parent.dataRef | |
if (!dataRef.children) {this.$set(dataRef, 'children', []) | |
} | |
dataRef.children.push({ | |
title: '地心侠士好玩, 值得分享', | |
key: new Date().getTime() | |
}) | |
}, |
5.2 增加上级
间接应用对象 dataRef
, 留神children
应用 $set
办法
// author:herbert date:20201101 qq:464884492 | |
eventAddSub() { | |
let dataRef = this.eventSelectedNode.dataRef | |
if (!dataRef.children) {this.$set(dataRef, 'children', []) | |
} | |
dataRef.children.push({ | |
title: '地心侠士, 还有很多 bug 欢送吐槽', | |
key: new Date().getTime(), | |
scopedSlots: {title: 'title'}, | |
children: []}) | |
}, |
5.3 批改节点
批改间接批改 dataRef
对应的值
// author:herbert date:20201101 qq:464884492 | |
eventModify() { | |
let dataRef = this.eventSelectedNode.dataRef | |
dataRef.title = '地心侠士, 今天及改完 bug' | |
}, |
5.4 删除节点
通过 vue 虚构 dom 找到父级 dataRef
, 从children
中移除选择项就能够
// author:herbert date:20201101 qq:464884492 | |
eventDelete() { | |
let parentDataRef = this.eventSelectedNode.$parent.dataRef | |
// 判断是否是顶层 | |
const children = parentDataRef.children | |
const currentDataRef = this.eventSelectedNode.dataRef | |
const index = children.indexOf(currentDataRef) | |
children.splice(index, 1) | |
} |
6. 总结
这个知识点, 从 demo 到最终实现. 前前后后破费快一个月的工夫. 期间查源码, 做测试, 很费时间. 不过把这个点说分明了, 我感觉很值得! 如果须要 Demo 源码请扫描下方的二维码, 关注公众号 [ 小院不小 ], 回复ant-tree
获取. 对于 ant-desgin-vue
这套组件库来说相比我以前应用的 easyUi
组件库来说, 感觉跟适宜网页展现一类. 做一些后盾零碎, 须要提供大量操作, 感觉还比拟乏力