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:464884492dataDriveAddSame() {   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:464884492dataDriveAddSub() {   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:464884492dataDriveModify() {   let selectItem = this.getTreeDataByKey(this.treeData, this.selectKeys[0])   selectItem.title = '扫码下方二维码,开始地心探险之旅'},
3.4 删除节点

删除和增加同级一样,须要找到父级节点children数组,曾经以后对象在父级数组中对应的索引.dataDriveDelete代码实现如下

// author:herbert date:20201030 qq:464884492dataDriveDelete() {   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:464884492slotAddSame(nodeItem) {console.log(nodeItem)this.$warn({ content: "采纳插槽形式,找不到父级对象,增加失败!不要想了,去玩地心侠士吧" })},
4.2 增加上级

尽管失去了对象,然而只是一个正本.所以设置children也是没用的!!

// author:herbert date:20201030 qq:464884492slotAddSub(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:464884492slotModify(nodeItem) {   console.log(nodeItem)   console.log('nodeItem仅仅时渲染Treenode属性的一个浅复制的正本,间接批改Title没有用')   nodeItem.title = '这里设置是没有用的,去玩游戏劳动一会吧'   // 这里能够借助dataRef 更新   nodeItem.dataRef.title = nodeItem.title },
4.4 删除节点

很显著,删除也是不能够的.

// author:herbert date:20201030 qq:464884492slodDelete(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:464884492eventAddSub() {   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组件库来说,感觉跟适宜网页展现一类.做一些后盾零碎,须要提供大量操作,感觉还比拟乏力