基于 ElementUI 封装的 Tree2
最近在使用自己封装的 Tree 的时候, 发现只有点击复选框的时候, 全选
复选框可以联动, 通过 setCheckedNodes
、 setCheckedKeys
和 default-checked-keys
设置目前勾选的节点时, 无法联动, 需要再优化
如下图所示:
通过 node
设置后, 全选并没有显示为半选状态
# 方法一(推荐, 以后只维护这个)
需要用到 check-change
这个 Event
节点选中状态发生变化时的回调
共三个参数, 依次为: 传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点
需要在这个方法里判断全选的状态, 但发现这个逻辑有点麻烦: 首先你要对传入的三个参数都需要判断, 还需要和 allNodes
做判断.
对, 其实不需要判断传入的三个参数, 直接拿到 allNodes
, 根据所有根节点的选中状态来处理全选的状态
但这个 Event
只要有节点变化就会触发, 像上图例子会调用好几次, 而且点击复选框的时候也会调用. 咱们在点击复选框的时候已经处理过逻辑了, 如果要使用 check-change
, 那么 check
这个事件就不能要了
<el-tree
:ref="ref"
v-bind="$attrs"
:node-key="nodeKey"
:show-checkbox="showCheckbox"
v-on="$listeners"
@check-change="handleCheckChange"
>
<slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot>
</el-tree>
2
3
4
5
6
7
8
9
10
data() {
return {
ref: 'elTree',
isIndeterminate: false,
isCheckAll: false
}
},
watch: {
defaultCheckedKeys: {
handler: 'handleCheckChange',
immediate: true
}
},
methods: {
handleCheckChange() {
if (!this.showCheckAll || !this.showCheckbox) {
return
}
// 防抖
this.debounce(
this.$nextTick(() => {
this.handleCheckAllStatus()
}),
100
)
},
// 防抖
debounce(func, wait) {
var timeout
return function() {
var context = this
var args = arguments
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
},
handleCheckAllStatus() {
const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore
._getAllNodes()
.filter(({ level, visible }) => level === 1 && visible)
// 关于 filter 的说明:
// 全选的状态其实只和根节点的状态有关, 而且也处理了 set 方法中 leafOnly 为 true 的情况
// visible 结合过滤使用
this.checkAll = allNodes.every(({ checked }) => checked)
this.isIndeterminate =
allNodes.some(({ indeterminate }) => indeterminate) ||
(allNodes.some(({ checked }) => checked) && !this.checkAll)
}
}
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 方法二
处理一下传入 el-tree
的 data
数据, 加上一个 全选
的根节点
<el-tree
:ref="ref"
v-bind="$attrs"
:data="treeData"
:node-key="nodeKey"
:show-checkbox="showCheckbox"
v-on="$listeners"
>
<slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot>
</el-tree>
2
3
4
5
6
7
8
9
10
props: {
data: {
type: Array,
default() {
return []
}
}
},
data() {
return {
treeData: [],
ref: 'elTree'
}
},
mounted() {
this.treeData =
this.showCheckAll && this.showCheckbox
? [
{
[this.$refs[this.ref].props.label]: '全选',
[this.$refs[this.ref].nodeKey || 'id']: 'rootId', // 这里可以灵活处理
[this.$refs[this.ref].props.children]: this.data
}
]
: this.data
}
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
好了, 组件封装完了. 全选的状态由 el-tree
内部处理, 咱们什么也不用管, 完成!
才怪嘞!!! 这样虽然简单, 但毕竟修改数据了, 一旦涉及到修改数据, 就会有很多麻烦需要处理.
比如: getChecked
这些获取选中节点的方法有可能会返回咱们添加的全选的数据, 而且全选的 check
和 check-change
也会触发(不过这个影响不算太大, 可以根据实际需求灵活应变)
那就只能修改 getChecked
方法, 先拿到当前选中的数据, 过滤掉 全选
的数据, 不能使用 el-tree
的默认方法了
data() {
return {
treeData: [],
ref: 'elTree',
checkAllId: '__rootId__'
}
},
watch: {
data: {
handler: 'handleData',
immediate: true
}
}
computed: {
isCheckAll() {
return this.showCheckAll && this.showCheckbox
}
},
methods: {
handleData() {
if (this.isCheckAll && this.data.length) {
this.treeData = [
{
[this.$refs[this.ref].props.label]: '全选',
[this.nodeKey]: this.checkAllId,
[this.$refs[this.ref].props.children]: this.data
}
]
} else {
this.treeData = this.data
}
},
getCheckedNodes(leafOnly, includeHalfChecked) {
if (this.isCheckAll) {
return this.$refs[this.ref]
.getCheckedNodes(leafOnly, includeHalfChecked)
.filter(node => node[this.nodeKey] !== this.checkAllId)
}
return this.$refs[this.ref].getCheckedNodes(leafOnly, includeHalfChecked)
},
getHalfCheckedNodes() {
if (this.isCheckAll) {
return this.$refs[this.ref]
.getHalfCheckedNodes()
.filter(node => node[this.nodeKey] !== this.checkAllId)
}
return this.$refs[this.ref].getHalfCheckedNodes()
},
getCheckedKeys(leafOnly) {
if (this.isCheckAll) {
return this.$refs[this.ref].getCheckedKeys(leafOnly).filter(key => key !== this.checkAllId)
}
return this.$refs[this.ref].getCheckedKeys(leafOnly)
},
getHalfCheckedKeys() {
if (this.isCheckAll) {
return this.$refs[this.ref].getHalfCheckedKeys().filter(key => key !== this.checkAllId)
}
return this.$refs[this.ref].getHalfCheckedKeys()
}
},
mounted() {
// 绑定 el-tree 方法
for (let key in this.$refs[this.ref]) {
if (!(key in this) && typeof this.$refs[this.ref][key] === 'function') {
this[key] = this.$refs[this.ref][key].bind(this.$refs[this.ref])
}
}
}
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 文本溢出显示省略号
刚好最近封装了一个文本溢出显示省略号的组件, 可以拿来一用
<slot slot-scope="{ node, data }" v-bind="{ node, data }">
<text-ellipsis :content="node.label"></text-ellipsis>
</slot>
2
3
# 单选时增加 disabled
el-tree
只对选择框处理了 disabled
, 单选时却没有, 所以我们需要增加一下
disabled
有两种方式:
- 通过
props
的disabled
字段和在data
中disabled
对应字段来禁用 - 通过
props
的disabled
方法来禁用
我们保持这种方式不变, 通过源码可知 disabled
保存在 node
里, 所以我们只需要在这里取即可
我们只需要做两件事:
- 设置
disabled
样式 - 当点击
disabled
节点后, 通过setCurrentKey
来重置当前选中节点
<el-tree
v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }"
>
<slot slot-scope="{ node, data }" v-bind="{ node, data }">
<text-ellipsis
:class="{ 'custom-disabled': node.disabled }"
:content="node.label"
></text-ellipsis>
</slot>
</el-tree>
2
3
4
5
6
7
8
9
10
这里使用 v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }"
是防止 emit
两遍 current-change
和 node-click
方法
.custom-disabled {
color: #94969a;
cursor: not-allowed;
}
2
3
4
// 处理单选的 disabled
handleCurrentChange(data, node) {
const { key, disabled } = node
if (disabled) {
// setCurrentKey 传入 null 或 undefined 时会清空选中节点
this.$refs[this.ref].setCurrentKey(this.currentKey)
return
}
this.currentKey = key
this.$emit('current-change', data, node)
},
handleNodeClick(data, node, self) {
const { disabled } = node
if (disabled) return
this.$emit('node-click', data, node, self)
}
// setCurrentKey 这两个方法中有一个调用就够了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
el-tree
节点还有右键事件和拖拽事件, 目前项目中没有用到, 就不处理 disabled
了, 如果有用到的话就需要处理一下
# 过滤时加载对应子节点
el-tree
不会返回过滤节点的子节点, 具体看这里: el-tree 节点过滤加载对应子节点 | Henry
所以我们需要自定义一下过滤方法
<el-tree
:filter-node-method="filterNodeMethod"
>
</el-tree>
2
3
4
props: {
filterNodeMethod: {
type: Function,
default(value, data, node) {
if (!value) return true
let parentNode = node.parent,
labels = [node.label],
level = 1
while (level < node.level) {
labels = [...labels, parentNode.label]
parentNode = parentNode.parent
level++
}
return labels.some(label => label.indexOf(value) !== -1)
}
}
},
methods: {
// 过滤
filter(value) {
this.$refs[this.ref].filter(value)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 单选只能选择叶子节点
最近在工作中经常遇到只能单选叶子节点的功能, 所以我们就需要封装一下.
以前在 SelectTree
中做过这个功能, 但其实应该是 Tree
的功能, 所以就搬到这里啦
基于 ElementUI 封装的 SelectTree | Henry
由以前经验可知需要分两种情况:
- 使用
el-tree
自带的node.isLeaf
判断是否是叶子节点 - 传入
isLeafMethod
函数来自定义叶子节点
而且处理逻辑与 disabled
类似, 就提取一个函数处理即可:
// 处理单选的 disabled 和 isLeaf
handleCurrentChange(data, node) {
const { key, disabled } = node
if (this.handleDisabled(disabled, data, node)) {
return
}
this.currentKey = key
this.$emit('current-change', data, node)
},
handleNodeClick(data, node, self) {
const { disabled } = node
if (this.handleDisabled(disabled, data, node)) {
return
}
this.$emit('node-click', data, node, self)
},
handleDisabled(disabled, data, node) {
if (
disabled ||
(this.isLeafMethod ? !this.isLeafMethod(data, node) : this.currentIsLeaf && !node.isLeaf)
) {
this.$refs[this.ref].setCurrentKey(this.currentKey)
return true
}
}
// setCurrentKey 这两个方法中有一个调用就够了
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
# 问题
# 方法一 data 为空数组, 全选仍存在(已解决)
由于全选标签只判断了 showCheckAll && showCheckbox
, 所以没数据的时候也是会显示的, 所以还需要判断是否有数据: v-if="showCheckAll && showCheckbox && data.length"
# 方法一 data 改变后, 显示有问题(已解决)
目前 data
改变后, 没有重新处理全选的状态, 导致全选选中状态有问题.
所以需要 watch data
; 而且发现 store._getAllNodes()
会保留历史数据: 比如第一次 data
就一条数据, _getAllNodes
获取的数据为一条, data
改变为两条数据后, _getAllNodes
获取的数据为三条
通过查看源码发现 nodesMap
保存所有数据, 只要注册节点并且该节点不在 nodesMap
中, 就添加, _getAllNodes
获取的就是这里的所有数据
所以 data
改变后, 需要重新加载 el-tree
:
<el-tree
:key="key"
>
2
3
watch: {
data() {
this.key = Math.random()
this.handleCheckChange()
}
}
2
3
4
5
6
这样修改后, 全选状态确实可以正常显示了, 但绑定的 el-tree
方法又出现问题了: 绑定方法只在 mounted
中绑定了一次, 通过修改 key
重新加载 el-tree
后, 绑定的方法仍然是重新加载前的, 而且重新绑定也不行; 有一些方法需要用到 store
, data
改变后, store
没有更新, 导致 getCheckedNodes
等方法仍获取的是第一次的值, 所以需要重新加载一下 tree
组件
所以不能在封装组件中通过修改 key
来重新加载组件, 会导致绑定的方法有问题
目前暂时在父组件中使用 v-if
控制, 以后看看有没有好方法, 这样上面的代码也不用加了
对比 data
改变前后通过 _getAllNodes
获取的数据发现, 改变前选中的数据, 改变后 checked
仍为 true
, 但通过 getCheckedKeys
获取的数据只有改变后选中的数据, 是不是可以查看源码看看这部分是如何实现的?
node_modules/element-ui/packages/tree/src/model/tree-store.js
getCheckedNodes(leafOnly = false, includeHalfChecked = false) {
const checkedNodes = [];
const traverse = function(node) {
const childNodes = node.root ? node.root.childNodes : node.childNodes;
childNodes.forEach((child) => {
if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) {
checkedNodes.push(child.data);
}
traverse(child);
});
};
traverse(this);
return checkedNodes;
}
getCheckedKeys(leafOnly = false) {
return this.getCheckedNodes(leafOnly).map((data) => (data || {})[this.key]);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
原来人家并没有用 _getAllNodes
获取全部数据, 而是通过 root
拿到所有根节点数据, 再依次遍历子节点来拿到所有选中的数据
那咱们也用 root
不就行了吗?
watch: {
data: {
handler: 'handleData',
immediate: true,
deep: true
}
},
methods: {
handleData() {
this.$nextTick(() => {
this.allNodes = this.getAllNodes(this.$refs[this.ref].root[childNodes])
this.allNodes.length &&
(this.maxLevel = Math.max.apply(
null,
this.allNodes.map(({ level }) => level)
))
this.$emit('max-level', this.maxLevel)
this.handleCheckChange()
return Promise.resolve()
})
},
getAllNodes() {
let allNodes = []
const traverse = function(node) {
const childNodes = node.root ? node.root.childNodes : node.childNodes
childNodes.forEach(child => {
allNodes.push(child)
traverse(child)
})
}
traverse(this.$refs[this.ref])
return allNodes
}
}
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
这次把 allNodes
缓存起来了, 避免每次都要获取一下, 由于是引用地址, 所以状态变化后会跟着改变的; 并且还 emit
出去了 max-level
, 省的还要在通过 getTreeMaxLevel
获取
# defaultExpandAll defaultExpandedKeys 无效(已解决)
由于目前在初始化的时候就执行了 expandToLevel
方法, 导致只展开到 level
级
而如果用户没有传入 level
, 只传入 defaultExpandAll
或 defaultExpandedKeys
, 默认 level
为 1
, 那只能展开到一级
所以需要添加 isFirst
判断: 默认为 true
, 调用 expandToLevel
时判断是否是第一次调用, 第一次调用的时候判断 defaultExpandAll
或 defaultExpandedKeys
是否有值, 有值的话就不执行后面的代码, 并且把 isFirst
置为 false
props: {
defaultExpandAll: {
type: Boolean,
default: false
},
defaultExpandedKeys: {
type: Array,
default() {
return []
}
}
},
data() {
return {
isFirst: true
}
},
methods: {
expandToLevel(level) {
if (this.isFirst && (this.defaultExpandAll || this.defaultExpandedKeys.length)) {
this.isFirst = false
return
}
this.isFirst = false
// do something
}
}
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