新项目中使用了ant-design-vue
组件库.该组件库彻底根基数据双向绑定的模式实现.只有表单组件提供少许的方法.因此,在使用ant-design-vue
时,必定要从改变数据的角度去切换UI显示效果.然而,在树形控件a-tree
的使用上,单从数据驱动上去考虑,感体验效果实在很差.javascript
经过阅读官方帮助文档,针对树形控件数据绑定.须要将数据构形成一个包含children,title,key
属性的大对象.这样一个对象,要么经过后端构造好这样的json对象,要么就是后端给前端一个json数组,前端根据上下级关系构建这么一个树形对象.数据绑定好,就能够成功的渲染成咱们想要的UI效果了.可痛点在哪里呢?html
以上操做,都要求不从新加载树形控件条件下完成.通过测试整理出了三个可行方案前端
咱们能够在帮助文档中找到名为selectedKeys(.sync)
属性,sync
表示该属性支持双向操做.可是,这里仅仅获取的是一个key
值,并非须要的绑定对象.因此,须要经过这key值找到这个对象.须要找这个对象就至关恶心了vue
因此,恶心的地方就在于构建好一个树,我又得遍历这个树查找某个节点,或者采用方案b这种空间换时间的作法java
这里咱们假设数据,已是构建成树形的数据格式.要实现数据驱动的首要任务须要完成两个核心方法node
getTreeDataByKey
getTreeParentChilds
两个方法代码分别以下json
// 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
},
复制代码
添加同级节点,须要把新增长的数据,添加到当前选中节点的父级的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
代码实现以下api
// 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()
})
},
复制代码
有了上边的基础,添加下级就很简单了.惟一须要注意的地方就是获取到的对象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()
},
复制代码
能获取到绑定对象,修改节点值也变得简单了,同添加下级同样使用getTreeDataByKey
获取当前对象,而后直接修改值就是了.dataDriveModify
代码实现以下
// author:herbert date:20201030 qq:464884492
dataDriveModify() {
let selectItem = this.getTreeDataByKey(this.treeData, this.selectKeys[0])
selectItem.title = '扫码下方二维码,开始地心探险之旅'
},
复制代码
删除和添加同级同样,须要找到父级节点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)
},
复制代码
在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>
复制代码
采用插槽的方式拿到对象,实际上是当前节点对应的属性值,而且是一个浅复制的副本.在源码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: "采用插槽方式,找不到父级对象,添加失败!不要想了,去玩地心侠士吧" })
},
复制代码
虽然获得了对象,可是只是一个副本.因此设置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: []
})
},
复制代码
修改同样也不能实现,不过上边有提到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
},
复制代码
很明显,删除也是不能够的.
// author:herbert date:20201030 qq:464884492
slodDelete(nodeItem) {
console.log(nodeItem)
this.$warn({ content: "采用插槽方式,找不到父级对象,删除失败!很明显,仍是去玩地心侠士吧" })
delete nodeItem.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方便后续的增长修改删除
利用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()
})
},
复制代码
直接使用对象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: []
})
},
复制代码
修改直接修改dataRef
对应的值
// author:herbert date:20201101 qq:464884492
eventModify() {
let dataRef = this.eventSelectedNode.dataRef
dataRef.title = '地心侠士,明天及改完bug'
},
复制代码
经过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)
}
复制代码
这个知识点,从demo到最终完成.前先后后花费快一个月的时间.期间查源码,作测试,很费时间.不过把这个点说清楚了,我以为很值得!若是须要Demo源码请扫描下方的二维码,关注公众号[小院不小],回复ant-tree
获取.关于ant-desgin-vue
这套组件库来讲相比我之前使用的easyUi
组件库来讲,感受跟适合网页展现一类.作一些后台系统,须要提供大量操做,感受还比较乏力