继上一篇 基于 ElementUI 封装的 SelectTree | Henry 后, 发现 tree 也需要封装一下, 增加一个全选功能和展开到 level 级的功能
源码在这里
展开 level 级
思路: 先找出 tree 的所有 node, 再根据传入的 level 来判断 node 是展开还是折叠状态
所以我们需要接收一个 level 的 props
props
1 | props: { |
watch
如果要动态改变展开的层级, 那必须 watch level
1 | watch: { |
methods
在这里我们可以获取所有 node: vue.js - element-ui 的 tree 树形组件怎么控制全部展开和全部折叠啊? - SegmentFault 思否
1 | const elTreeStore = this.$refs[this.ref].store |
在 node 中发现 expanded 和 level 这两个属性, 那么比较 node 和 props 的 level , 然后根据结果修改 expanded 这个属性的值应该就可以控制展开折叠了吧
1 | /** |
完全符合预期的效果
延伸
在 node 的 __proto__ 中发现两个方法: expand 和 collapse, 这不就是展开和折叠吗?

查看一下源码:
1 | Node.prototype.expand = function expand(callback, expandParent) { |
看起来 collapse 做的事情和我们是一样的, 那就还是用我们自己的吧
expand 除了将当前节点和全部父节点展开以外, 貌似还和懒加载有关, 难道我们的 node.expanded = true 无法对懒加载的数据进行展开折叠吗?
那我们就拿官方的示例来试一下吧
1 | <tree :props="props" :load="loadNode" :level="level" lazy show-checkbox></tree> |
1 | level: 1, |
果然无法展开折叠, 那我们使用 expand 方法呢?
1 | expandToLevel(level) { |
发现也不行
原来是_getAllNodes 获取的是空数组, 那 forEach 里面的逻辑就不执行了呀
是因为没有默认的 data 吗? 加一个
1 | <tree |
发现还是空数组.
这就难办了呀, 无法获取全部数据, 那怎么控制展开折叠呀
目前本人没有找到有效的方法获取懒加载的所有数据, 话说好像也不好获取吧, 应该说没法获取吧. 懒加载的数据都是在点击节点后才加载的, 初始化应该是拿不到的
那这样看来, expand 方法目前和咱们的结果是一样的, 而且 expand 还将父节点也展开了, 而咱们在父节点上也调用了这个方法, 明显就是重了.
那关于展开的方式现在有三种:
- 直接使用
node.expanded = true, 方便省事 - 使用
expand方法, 但在每一个node上使用会有效率问题, 重复给父节点的expanded赋值, 当然也不会有太大的影响 - 如果是展开全部, 使用
expand方法, 否则使用node.expanded = true
关于 方式3 的说明: expand 存在效率问题, 但可以根据 isLeaf 找出每一个树的最末节点的, 只要在这个节点调用方法即可; 如果是展开到某一级, 如果使用 expand, 那么必须根据 level 找出每一个树相对于 level 的末节点(比如 tree 有三个根节点, 第一个根节点有三级, 另两个有两级, 那么当 level = 3 时, 第一个的末节点就是 3, 而另外两个就是 2), 这样才不会重复赋值, 但找这个末节点有点麻烦, 还不如直接给符合展开条件的节点直接赋值快呢
代码
1 | expandToLevel(level) { |
存在的问题
- 无法对懒加载进行展开折叠
- 如果用户不传
node-key, 默认值为空,_getAllNodes获取到的是一个空数组, 后面逻辑都无法进行下去了, 那就只能是在props中给一个默认值了
全选
思路: 首先要在最顶层增加一个 checkbox, 要有 全选/半选/未选中 三种状态, 要和 tree 的 checkbox 相互联动
这个也需要参数来判断是否显示全选按钮
props
1 | props: { |
结构样式
在最顶层增加一个 checkbox, 正好 element-ui 提供 indeterminate 状态
1 | <el-checkbox |
1 | .b-tree-check-all { |
methods
首先处理全选按钮的逻辑:
isIndeterminate的值只能是falseemit check事件- 根据是否全选来判断是否需要获取所有
node, 使用setCheckedKeys对tree进行选中处理
关于只 emit check 事件而不同时 emit check-change 的说明: check-change 受 render-after-expand 的影响, 如果 render-after-expand = true, 那么第一次如果没有展开, 点击父节点只拿到父节点的 check-change 事件, 如果展开以后, 点击父节点, 就会拿到父节点和子节点的 check-change 事件. 目前好像没有业务需要这样的, 感觉 check 的数据就足以, 如果以后需要再加上
1 | // 处理全选 |
再处理 tree 的选中逻辑:
- 首先判断
showCheckAll和showCheckbox, 如果为false, 直接return - 根据
checkedKeys.length和allNodes.length比较来给checkAll和isIndeterminate赋值
1 | // el-tree 复选框被点击 |
存在的问题
一: element-ui 的 filter, 只是将数据在页面隐藏, 使用 _getAllNodes 仍然能获取到, 所以还需要过滤一下
1 | const elTreeStore = this.$refs[this.ref].store |
虽然咱们自己的全选使用过滤实现了页面与实际数据相同, 但 element-ui 自己仍然有 bug, 具体请看这里: [Bug Report] with tree components, multi box filter, select the parent node, can only choose to filter out of the current node? · Issue #18522 · ElemeFE/element
二: 只有点击复选框的时候, 全选 复选框可以联动, 通过 setCheckedNodes 、 setCheckedKeys 和 default-checked-keys 设置目前勾选的节点时, 无法联动, 具体解决方案请查看 基于 ElementUI 封装的 Tree2 | Henry
Attributes/Events/slot/方法
$attrs
如果看过本人封装 SelectTree 的那篇文章的话, 大家应该知道 v-bind=obj 可以传入一个对象的所有属性, 但最近浏览 Vue 官网发现 $attrs:
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建高级别的组件时非常有用。
$listeners
还有 $listeners:
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件——在创建更高层次的组件时非常有用。
那么 el-tree 进行封装后:
1 | <el-tree |
由于 show-checkbox 已经在 props 中声明了, 那么这里还需要写一下
slot
由于 el-tree 支持 scoped slot 的自定义节点内容, 那咱们也得要实现一下这个
首先咱们需要一个标签来拿到 el-tree 的两个参数 node 和 data, 而且还需要一个 slot 标签来将传入咱们封装组件的 scoped slot 接收, 并将拿到的两个参数 node 和 data 传递出去.
阅读 插槽 — Vue.js 后发现可以合并到一个 slot 标签上:
1 | <slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot> |
slot-scope 接收 node 和 data, v-bind 传递出去, 展示默认值
方法
本人以为有 $attrs 和 $listeners, 那方法是不是也有这么一个属性呢? 但没有在 Vue 官网发现这个参数, 如果有大佬找到, 望不吝赐教.
而且也在微信群和网上问过大佬: javascript - vue 封装组件时候如何向上传递方法? - SegmentFault 思否, 大部分人都是说 $refs.$refs 这么一直通过 refs 找到 el-tree, 但总感觉这种方式不太优雅; 还有大佬说找到 el-tree 后可以缓存起来, 但封装 SelectTree 的时候发现缓存会有问题, 而且本质还是上面那种方法, 所以还是不想使用
而我本人想到的最笨的办法是将 el-tree 的所有方法在封装组件中挨个写一遍, 类似下面这种:
1 | getCheckedNodes(leafOnly, includeHalfChecked) { |
首先不说有多麻烦, 万一以后 el-tree 又增加或删除方法, 我还得参考官网维护, 这个成本就有点大了
终于在 vue.js - 如何获取一个vue实例里的所有方法 - SegmentFault 思否 这个问答中找到了一种方式, 成功解决了问题, 虽然本人认为不是最优雅的解法, 但对比上面的还是好很多了.
以下再粘贴一下本人的回答吧
就以 el-tree 为例来说吧, 以下均为简略代码
Tree.vue
1 | <el-tree |
1 | import { Tree } from 'element-ui' |
MyTree.vue
1 | <tree |
1 | import Tree from 'plugins/Tree' |
输出:
1 | this.$refs.tree ƒ getCheckedNodes(leafOnly, includeHalfChecked) { |
this.store 的 this 是 MyTree 这个实例, 就没有 store 这个属性, 这个属性是在 el-tree 上的.
所以这种方式是不行的, 因为 this 指向不同, 而我也没找到好办法可以在这种绑定 this, 如果有大佬知道, 望不吝赐教
那就只能是在 mounted 上来绑定 this 了
1 | mounted() { |
输出 key 看了一下, el-tree 官方文档上的方法都有, 好像还多了两个, 不过不影响使用了, 而且这样也不用再单独引入 Tree 了
至此, 也算 “完美” 解决问题了, 但还是感觉不是太优雅. 比起 $refs.$refs.$refs 来说还是这种好一点