基于 ElementUI 封装的 Tree
继上一篇 基于 ElementUI 封装的 SelectTree | Henry 后, 发现 tree
也需要封装一下, 增加一个全选功能和展开到 level
级的功能
# 展开 level 级
思路: 先找出 tree
的所有 node
, 再根据传入的 level
来判断 node
是展开还是折叠状态
所以我们需要接收一个 level
的 props
# props
props: {
level: {
type: Number,
default: 1
}
}
2
3
4
5
6
# watch
如果要动态改变展开的层级, 那必须 watch level
watch: {
level: {
handler: 'expandToLevel',
immediate: true
}
}
2
3
4
5
6
# methods
在这里我们可以获取所有 node
: vue.js - element-ui 的 tree 树形组件怎么控制全部展开和全部折叠啊? - SegmentFault 思否 (opens new window)
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
console.log('TCL: expandToLevel -> allNodes', allNodes)
2
3
在 node
中发现 expanded
和 level
这两个属性, 那么比较 node
和 props
的 level
, 然后根据结果修改 expanded
这个属性的值应该就可以控制展开折叠了吧
/**
* @method 展开至指定层级
* @param {Number} level 要展开至几级?0, 1, 2, 3...
**/
expandToLevel(level) {
this.$nextTick(() => {
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
console.log('TCL: expandToLevel -> allNodes', allNodes)
if (level === 0) {
// 展开全部
allNodes.forEach(node => {
node.expanded = true
})
} else {
allNodes.forEach(node => {
if (node.level >= level) {
node.expanded = false
} else {
node.expanded = true
}
})
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
完全符合预期的效果
# 延伸
在 node
的 __proto__
中发现两个方法: expand
和 collapse
, 这不就是展开和折叠吗?
查看一下源码:
Node.prototype.expand = function expand(callback, expandParent) {
var _this = this;
var done = function done() {
if (expandParent) {
var parent = _this.parent;
while (parent.level > 0) {
parent.expanded = true;
parent = parent.parent;
}
}
_this.expanded = true;
if (callback) callback();
};
if (this.shouldLoadData()) {
this.loadData(function (data) {
if (data instanceof Array) {
if (_this.checked) {
_this.setChecked(true, true);
} else if (!_this.store.checkStrictly) {
reInitChecked(_this);
}
done();
}
});
} else {
done();
}
};
Node.prototype.collapse = function collapse() {
this.expanded = false;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
看起来 collapse
做的事情和我们是一样的, 那就还是用我们自己的吧
expand
除了将当前节点和全部父节点展开以外, 貌似还和懒加载有关, 难道我们的 node.expanded = true
无法对懒加载的数据进行展开折叠吗?
那我们就拿官方的示例来试一下吧
<tree :props="props" :load="loadNode" :level="level" lazy show-checkbox></tree>
level: 1,
props: {
label: 'name',
children: 'zones',
isLeaf: 'leaf'
}
loadNode(node, resolve) {
console.log('TCL: loadNode -> node, resolve', node, resolve)
if (node.level === 0) {
return resolve([{ name: 'region' }])
}
if (node.level > 1) return resolve([])
setTimeout(() => {
const data = [
{
name: 'leaf',
leaf: true
},
{
name: 'zone'
}
]
resolve(data)
}, 500)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
果然无法展开折叠, 那我们使用 expand
方法呢?
expandToLevel(level) {
this.$nextTick(() => {
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
console.log('TCL: expandToLevel -> allNodes', allNodes)
if (level === 0) {
// 展开全部
allNodes.forEach(node => {
node.expand(null, true)
})
} else {
allNodes.forEach(node => {
if (node.level >= level) {
node.collapse()
} else {
node.expand(null, false)
}
})
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
发现也不行
原来是_getAllNodes
获取的是空数组, 那 forEach
里面的逻辑就不执行了呀
是因为没有默认的 data
吗? 加一个
<tree
:data="[{ name: 'region11' }, { name: 'aa11' }]"
:props="props"
:load="loadNode"
:level="level"
lazy
show-checkbox
></tree>
2
3
4
5
6
7
8
发现还是空数组.
这就难办了呀, 无法获取全部数据, 那怎么控制展开折叠呀
目前本人没有找到有效的方法获取懒加载的所有数据, 话说好像也不好获取吧, 应该说没法获取吧. 懒加载的数据都是在点击节点后才加载的, 初始化应该是拿不到的
那这样看来, expand
方法目前和咱们的结果是一样的, 而且 expand
还将父节点也展开了, 而咱们在父节点上也调用了这个方法, 明显就是重了.
那关于展开的方式现在有三种:
- 直接使用
node.expanded = true
, 方便省事 - 使用
expand
方法, 但在每一个node
上使用会有效率问题, 重复给父节点的expanded
赋值, 当然也不会有太大的影响 - 如果是展开全部, 使用
expand
方法, 否则使用node.expanded = true
关于 方式3
的说明: expand
存在效率问题, 但可以根据 isLeaf
找出每一个树的最末节点的, 只要在这个节点调用方法即可; 如果是展开到某一级, 如果使用 expand
, 那么必须根据 level
找出每一个树相对于 level
的末节点(比如 tree
有三个根节点, 第一个根节点有三级, 另两个有两级, 那么当 level = 3
时, 第一个的末节点就是 3
, 而另外两个就是 2
), 这样才不会重复赋值, 但找这个末节点有点麻烦, 还不如直接给符合展开条件的节点直接赋值快呢
# 代码
expandToLevel(level) {
this.$nextTick(() => {
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
if (level === 0) {
// 展开全部
allNodes.forEach(node => {
node.isLeaf && node.expand(null, true)
})
} else {
allNodes.forEach(node => {
if (node.level >= level) {
node.expanded = false
} else {
node.expanded = true
}
})
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 存在的问题
- 无法对懒加载进行展开折叠
- 如果用户不传
node-key
, 默认值为空,_getAllNodes
获取到的是一个空数组, 后面逻辑都无法进行下去了, 那就只能是在props
中给一个默认值了
# 全选
思路: 首先要在最顶层增加一个 checkbox
, 要有 全选/半选/未选中
三种状态, 要和 tree
的 checkbox
相互联动
这个也需要参数来判断是否显示全选按钮
# props
props: {
showCheckAll: {
type: Boolean,
default: false
},
showCheckbox: {
type: Boolean,
default: false
}
}
2
3
4
5
6
7
8
9
10
# 结构样式
在最顶层增加一个 checkbox
, 正好 element-ui
提供 indeterminate
状态 (opens new window)
<el-checkbox
v-if="showCheckAll && showCheckbox"
class="b-tree-check-all"
:indeterminate="isIndeterminate"
v-model="checkAll"
@change="handleCheckAllChange"
>全选</el-checkbox
>
2
3
4
5
6
7
8
.b-tree-check-all {
padding-left: 8px;
font-weight: normal;
.el-checkbox__label {
color: #606266;
}
}
2
3
4
5
6
7
# 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
的数据就足以, 如果以后需要再加上
// 处理全选
handleCheckAllChange() {
this.isIndeterminate = false
let checkedKeys = []
if (this.checkAll) {
const elTreeStore = this.$refs[this.ref].store
const checkedNodes = elTreeStore._getAllNodes()
checkedKeys = checkedNodes.map(({ key }) => key)
this.$emit(
'check',
{ [this.$refs[this.ref].props.label || 'label']: '全选' },
{
checkedNodes,
checkedKeys,
halfCheckedNodes: [],
halfCheckedKeys: []
}
)
} else {
this.$emit(
'check',
{ [this.$refs[this.ref].props.label || 'label']: '全选' },
{
checkedNodes: [],
checkedKeys: [],
halfCheckedNodes: [],
halfCheckedKeys: []
}
)
}
this.$refs[this.ref].setCheckedKeys(checkedKeys)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
再处理 tree
的选中逻辑:
- 首先判断
showCheckAll
和showCheckbox
, 如果为false
, 直接return
- 根据
checkedKeys.length
和allNodes.length
比较来给checkAll
和isIndeterminate
赋值
// el-tree 复选框被点击
handleCheck(data, checked) {
if (!this.showCheckAll || !this.showCheckbox) {
return
}
const { checkedKeys } = checked
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes()
if (checkedKeys.length) {
if (checkedKeys.length === allNodes.length) {
this.checkAll = true
this.isIndeterminate = false
} else {
this.checkAll = false
this.isIndeterminate = true
}
} else {
this.isIndeterminate = false
this.checkAll = false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 存在的问题
一: element-ui
的 filter
, 只是将数据在页面隐藏, 使用 _getAllNodes
仍然能获取到, 所以还需要过滤一下
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().filter(node => node.visible)
2
虽然咱们自己的全选使用过滤实现了页面与实际数据相同, 但 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 (opens new window)
二: 只有点击复选框的时候, 全选
复选框可以联动, 通过 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
进行封装后:
<el-tree
:ref="ref"
v-bind="$attrs"
:show-checkbox="showCheckbox"
v-on="$listeners"
@check="handleCheck"
>
</el-tree>
2
3
4
5
6
7
8
由于 show-checkbox
已经在 props
中声明了, 那么这里还需要写一下
# slot
由于 el-tree
支持 scoped slot
的自定义节点内容, 那咱们也得要实现一下这个
首先咱们需要一个标签来拿到 el-tree
的两个参数 node
和 data
, 而且还需要一个 slot
标签来将传入咱们封装组件的 scoped slot
接收, 并将拿到的两个参数 node
和 data
传递出去.
阅读 插槽 — Vue.js (opens new window) 后发现可以合并到一个 slot
标签上:
<slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot>
slot-scope
接收 node
和 data
, v-bind
传递出去, node.label
展示默认值
# 方法
本人以为有 $attrs
和 $listeners
, 那方法是不是也有这么一个属性呢? 但没有在 Vue
官网发现这个参数, 如果有大佬找到, 望不吝赐教.
而且也在微信群和网上问过大佬: javascript - vue 封装组件时候如何向上传递方法? - SegmentFault 思否 (opens new window), 大部分人都是说 $refs.$refs
这么一直通过 refs
找到 el-tree
, 但总感觉这种方式不太优雅; 还有大佬说找到 el-tree
后可以缓存起来, 但封装 SelectTree
的时候发现缓存会有问题, 而且本质还是上面那种方法, 所以还是不想使用
而我本人想到的最笨的办法是将 el-tree
的所有方法在封装组件中挨个写一遍, 类似下面这种:
getCheckedNodes(leafOnly, includeHalfChecked) {
return this.$refs[this.ref].getCheckedNodes(leafOnly, includeHalfChecked);
}
2
3
首先不说有多麻烦, 万一以后 el-tree
又增加或删除方法, 我还得参考官网维护, 这个成本就有点大了
终于在 vue.js - 如何获取一个 vue 实例里的所有方法 - SegmentFault 思否 (opens new window) 这个问答中找到了一种方式, 成功解决了问题, 虽然本人认为不是最优雅的解法, 但对比上面的还是好很多了.
以下再粘贴一下本人的回答吧
就以 el-tree
为例来说吧, 以下均为简略代码
Tree.vue
<el-tree ref="elTree"></el-tree>
import { Tree } from 'element-ui'
export default {
name: 'Tree',
components: { [Tree.name]: Tree },
methods: {
// 向上传递 el-tree 的方法
...Tree.methods
}
}
2
3
4
5
6
7
8
9
10
MyTree.vue
<tree ref="tree" v-bind="treeProps"></tree>
import Tree from 'plugins/Tree'
getCheckedNodes() {
console.log('this.$refs.tree', this.$refs.tree.getCheckedNodes)
}
2
3
4
5
输出:
this.$refs.tree ƒ getCheckedNodes(leafOnly, includeHalfChecked) {
return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
}
2
3
this.store
的 this
是 MyTree
这个实例, 就没有 store
这个属性, 这个属性是在 el-tree
上的.
所以这种方式是不行的, 因为 this
指向不同, 而我也没找到好办法可以在这种绑定 this
, 如果有大佬知道, 望不吝赐教
那就只能是在 mounted
上来绑定 this
了
mounted() {
for (let key in this.$refs.elTree) {
if (!(key in this) && typeof this.$refs.elTree[key] === 'function') {
this[key] = this.$refs.elTree[key].bind(this.$refs.elTree)
console.log('TCL: mounted -> key', key)
}
}
}
2
3
4
5
6
7
8
输出 key
看了一下, el-tree
官方文档上的方法都有, 好像还多了两个, 不过不影响使用了, 而且这样也不用再单独引入 Tree
了
至此, 也算 "完美" 解决问题了, 但还是感觉不是太优雅. 比起 $refs.$refs.$refs
来说还是这种好一点