我们在键盘上留下的余温, 也将随时代传递到更远的将来


:D 获取中...

继上一篇 基于 ElementUI 封装的 SelectTree | Henry 后, 发现 tree 也需要封装一下, 增加一个全选功能和展开到 level 级的功能

源码在这里

展开 level 级

思路: 先找出 tree 的所有 node, 再根据传入的 level 来判断 node 是展开还是折叠状态

所以我们需要接收一个 levelprops

props

1
2
3
4
5
6
props: {
level: {
type: Number,
default: 1
}
}

watch

如果要动态改变展开的层级, 那必须 watch level

1
2
3
4
5
6
watch: {
level: {
handler: 'expandToLevel',
immediate: true
}
}

methods

在这里我们可以获取所有 node: vue.js - element-ui 的 tree 树形组件怎么控制全部展开和全部折叠啊? - SegmentFault 思否

1
2
3
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
console.log('TCL: expandToLevel -> allNodes', allNodes)

node 中发现 expandedlevel 这两个属性, 那么比较 nodepropslevel , 然后根据结果修改 expanded 这个属性的值应该就可以控制展开折叠了吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @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
}
})
}
})
}

完全符合预期的效果

延伸

node__proto__ 中发现两个方法: expandcollapse, 这不就是展开和折叠吗?

__proto__

查看一下源码:

1
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
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;
};

看起来 collapse 做的事情和我们是一样的, 那就还是用我们自己的吧

expand 除了将当前节点和全部父节点展开以外, 貌似还和懒加载有关, 难道我们的 node.expanded = true 无法对懒加载的数据进行展开折叠吗?

那我们就拿官方的示例来试一下吧

1
<tree :props="props" :load="loadNode" :level="level" lazy show-checkbox></tree>
1
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
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)
}

果然无法展开折叠, 那我们使用 expand 方法呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
}
})
}
})
}

发现也不行

原来是_getAllNodes 获取的是空数组, 那 forEach 里面的逻辑就不执行了呀

是因为没有默认的 data 吗? 加一个

1
2
3
4
5
6
7
8
<tree
:data="[{ name: 'region11' }, { name: 'aa11' }]"
:props="props"
:load="loadNode"
:level="level"
lazy
show-checkbox
></tree>

发现还是空数组.

这就难办了呀, 无法获取全部数据, 那怎么控制展开折叠呀

目前本人没有找到有效的方法获取懒加载的所有数据, 话说好像也不好获取吧, 应该说没法获取吧. 懒加载的数据都是在点击节点后才加载的, 初始化应该是拿不到的

那这样看来, expand 方法目前和咱们的结果是一样的, 而且 expand 还将父节点也展开了, 而咱们在父节点上也调用了这个方法, 明显就是重了.

那关于展开的方式现在有三种:

  1. 直接使用 node.expanded = true, 方便省事
  2. 使用 expand 方法, 但在每一个 node 上使用会有效率问题, 重复给父节点的 expanded 赋值, 当然也不会有太大的影响
  3. 如果是展开全部, 使用 expand 方法, 否则使用 node.expanded = true

关于 方式3 的说明: expand 存在效率问题, 但可以根据 isLeaf 找出每一个树的最末节点的, 只要在这个节点调用方法即可; 如果是展开到某一级, 如果使用 expand, 那么必须根据 level 找出每一个树相对于 level 的末节点(比如 tree 有三个根节点, 第一个根节点有三级, 另两个有两级, 那么当 level = 3 时, 第一个的末节点就是 3, 而另外两个就是 2), 这样才不会重复赋值, 但找这个末节点有点麻烦, 还不如直接给符合展开条件的节点直接赋值快呢

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
})
}
})
}

存在的问题

  1. 无法对懒加载进行展开折叠
  2. 如果用户不传 node-key, 默认值为空, _getAllNodes 获取到的是一个空数组, 后面逻辑都无法进行下去了, 那就只能是在 props 中给一个默认值了

全选

思路: 首先要在最顶层增加一个 checkbox, 要有 全选/半选/未选中 三种状态, 要和 treecheckbox 相互联动

这个也需要参数来判断是否显示全选按钮

props

1
2
3
4
5
6
7
8
9
10
props: {
showCheckAll: {
type: Boolean,
default: false
},
showCheckbox: {
type: Boolean,
default: false
}
}

结构样式

在最顶层增加一个 checkbox, 正好 element-ui 提供 indeterminate 状态

1
2
3
4
5
6
7
8
<el-checkbox
v-if="showCheckAll && showCheckbox"
class="b-tree-check-all"
:indeterminate="isIndeterminate"
v-model="checkAll"
@change="handleCheckAllChange"
>全选</el-checkbox
>
1
2
3
4
5
6
7
.b-tree-check-all {
padding-left: 8px;
font-weight: normal;
.el-checkbox__label {
color: #606266;
}
}

methods

首先处理全选按钮的逻辑:

  1. isIndeterminate 的值只能是 false
  2. emit check 事件
  3. 根据是否全选来判断是否需要获取所有 node, 使用 setCheckedKeystree 进行选中处理

关于只 emit check 事件而不同时 emit check-change 的说明: check-changerender-after-expand 的影响, 如果 render-after-expand = true, 那么第一次如果没有展开, 点击父节点只拿到父节点的 check-change 事件, 如果展开以后, 点击父节点, 就会拿到父节点和子节点的 check-change 事件. 目前好像没有业务需要这样的, 感觉 check 的数据就足以, 如果以后需要再加上

1
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
// 处理全选
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)
}

再处理 tree 的选中逻辑:

  1. 首先判断 showCheckAllshowCheckbox, 如果为 false, 直接 return
  2. 根据 checkedKeys.lengthallNodes.length 比较来给 checkAllisIndeterminate 赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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
}
}

存在的问题

一: element-uifilter, 只是将数据在页面隐藏, 使用 _getAllNodes 仍然能获取到, 所以还需要过滤一下

1
2
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().filter(node => node.visible)

虽然咱们自己的全选使用过滤实现了页面与实际数据相同, 但 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

二: 只有点击复选框的时候, 全选 复选框可以联动, 通过 setCheckedNodessetCheckedKeysdefault-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
2
3
4
5
6
7
8
<el-tree
:ref="ref"
v-bind="$attrs"
:show-checkbox="showCheckbox"
v-on="$listeners"
@check="handleCheck"
>
</el-tree>

由于 show-checkbox 已经在 props 中声明了, 那么这里还需要写一下

slot

由于 el-tree 支持 scoped slot 的自定义节点内容, 那咱们也得要实现一下这个

首先咱们需要一个标签来拿到 el-tree 的两个参数 nodedata, 而且还需要一个 slot 标签来将传入咱们封装组件的 scoped slot 接收, 并将拿到的两个参数 nodedata 传递出去.

阅读 插槽 — Vue.js 后发现可以合并到一个 slot 标签上:

1
<slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot>

slot-scope 接收 nodedata, v-bind 传递出去, 展示默认值

方法

本人以为有 $attrs$listeners, 那方法是不是也有这么一个属性呢? 但没有在 Vue 官网发现这个参数, 如果有大佬找到, 望不吝赐教.

而且也在微信群和网上问过大佬: javascript - vue 封装组件时候如何向上传递方法? - SegmentFault 思否, 大部分人都是说 $refs.$refs 这么一直通过 refs 找到 el-tree, 但总感觉这种方式不太优雅; 还有大佬说找到 el-tree 后可以缓存起来, 但封装 SelectTree 的时候发现缓存会有问题, 而且本质还是上面那种方法, 所以还是不想使用

而我本人想到的最笨的办法是将 el-tree 的所有方法在封装组件中挨个写一遍, 类似下面这种:

1
2
3
getCheckedNodes(leafOnly, includeHalfChecked) {
return this.$refs[this.ref].getCheckedNodes(leafOnly, includeHalfChecked);
}

首先不说有多麻烦, 万一以后 el-tree 又增加或删除方法, 我还得参考官网维护, 这个成本就有点大了

终于在 vue.js - 如何获取一个vue实例里的所有方法 - SegmentFault 思否 这个问答中找到了一种方式, 成功解决了问题, 虽然本人认为不是最优雅的解法, 但对比上面的还是好很多了.

以下再粘贴一下本人的回答吧

就以 el-tree 为例来说吧, 以下均为简略代码

Tree.vue

1
2
3
<el-tree
ref="elTree"
></el-tree>
1
2
3
4
5
6
7
8
9
10
import { Tree } from 'element-ui'

export default {
name: 'Tree',
components: { [Tree.name]: Tree },
methods: {
// 向上传递 el-tree 的方法
...Tree.methods
}
}

MyTree.vue

1
2
3
4
<tree
ref="tree"
v-bind="treeProps"
></tree>
1
2
3
4
5
import Tree from 'plugins/Tree'

getCheckedNodes() {
console.log('this.$refs.tree', this.$refs.tree.getCheckedNodes)
}

输出:

1
2
3
this.$refs.tree ƒ getCheckedNodes(leafOnly, includeHalfChecked) {
return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
}

this.storethisMyTree 这个实例, 就没有 store 这个属性, 这个属性是在 el-tree 上的.

所以这种方式是不行的, 因为 this 指向不同, 而我也没找到好办法可以在这种绑定 this, 如果有大佬知道, 望不吝赐教

那就只能是在 mounted 上来绑定 this

1
2
3
4
5
6
7
8
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)
}
}
}

输出 key 看了一下, el-tree 官方文档上的方法都有, 好像还多了两个, 不过不影响使用了, 而且这样也不用再单独引入 Tree

至此, 也算 “完美” 解决问题了, 但还是感觉不是太优雅. 比起 $refs.$refs.$refs 来说还是这种好一点


 评论

 无法加载Disqus评论系统,请确保您的网络能够正常访问。