继上一篇 基于 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
的值只能是false
emit 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
来说还是这种好一点