乐趣区

关于ant-design:antdesignvue中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 组件库来说, 感觉跟适宜网页展现一类. 做一些后盾零碎, 须要提供大量操作, 感觉还比拟乏力

退出移动版