新項目中使用了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
組件庫來講,感受跟適合網頁展現一類.作一些後臺系統,須要提供大量操做,感受還比較乏力