ant-design-vue中tree增刪改

ant-design-vue中tree增刪改

 1. 使用背景

新項目中使用了ant-design-vue組件庫.該組件庫完全根基數據雙向綁定的模式實現.只有表單組件提供少量的方法.所以,在使用ant-design-vue時,一定要從改變數據的角度去切換UI顯示效果.然而,在樹形控件a-tree的使用上,單從數據驅動上去考慮,感體驗效果實在不好.

2. 當前痛點

通過閱讀官方幫助文檔,針對樹形控件數據綁定.需要將數據構造成一個包含children,title,key屬性的大對象.這樣一個對象,要麼通過後端構造好這樣的json對象,要麼就是後端給前端一個json數組,前端根據上下級關係構建這麼一個樹形對象.數據綁定好,就可以成功的渲染成我們想要的UI效果了.可痛點在哪裏呢?

  • 樹形加載成功後,我要向當前的樹形添加一個同級以及下級節點該如何操作(增)
  • 樹形加載成功後,我要修改任意一個樹形節點該如何操作(改)
  • 樹形加載成功後,我要刪除一個樹形節點該如何操作(刪)

以上操作,都要求不重新加載樹形控件條件下完成.經過測試整理出了三個可行方案

  1. 數據驅動
  2. 作用域插槽
  3. 節點事件

3. 數據驅動實現樹形節點增刪改

我們可以在幫助文檔中找到名爲selectedKeys(.sync)屬性,sync表示該屬性支持雙向操作.但是,這裏僅僅獲取的是一個key值,並不是需要的綁定對象.所以,需要通過這key值找到這個對象.需要找這個對象就相當噁心了

  1. 如果後端返回是構建好的數據,需要遍歷這個樹形數據中找到和這個key值對應的對象.我能想到的就是通過頂層節點遞歸查找.可是控件都渲染完成了,都知道每個節點的數據.我爲什要重新查找一遍呢???
  2. 如果後端返回的僅僅是一個數組,這個剛纔有提到需要重新構建這部分數據爲對象.這樣查找這個對象又分兩種情況
    a. 如果列表數據和構建後樹形對象採用克隆的方式,也就是列表中對象的地址和樹形中相同key值對象的地址不同.需要通過方法1遍歷重新構造後的樹形數據
    b. 如果列表數據中的對象和構建後對應的節點是相同的對象地址.可以直接查找這個列表數據得到對應的對象.

所以,噁心的地方就在於構建好一個樹,我又得遍歷這個樹查找某個節點,或者採用方案b這種空間換時間的做法

這裏我們假設數據,已經是構建成樹形的數據格式.要實現數據驅動的首要任務需要完成兩個核心方法

  1. 根據當前節點key值查找節點對象getTreeDataByKey
  2. 根據當前節點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);
},

從代碼中可以看到TreeNodeonSelect其實是調用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組件庫來說,感覺跟適合網頁展示一類.做一些後臺系統,需要提供大量操作,感覺還比較乏力

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章