ant-design-vue中tree增刪改
- 2020 年 11 月 2 日
- 筆記
- antvue, javascript, VUE, 前端
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
組件庫來說,感覺跟適合網頁展示一類.做一些後台系統,需要提供大量操作,感覺還比較乏力