基于 ElementUI 封装的 SelectTree
最近重构项目, 遇到一个需要 SelectTree
的组件, 就在网上找了一圈, 发现 Element-UI 二次封装实现 TreeSelect 树形下拉选择组件 - sleepwalker_1992 的专栏 - CSDN 博客 (opens new window)这个还不错, 但奈何作者不更新了, 而且现在样式有点问题, 就想着参考大佬的思路自己也封装一下
话不多说, 先将本人源码 (opens new window)奉上. 好了, 开干!
# 分析大佬源码
<template>
<div>
<div class="mask" v-show="isShowSelect" @click="isShowSelect = !isShowSelect"></div>
<el-popover
placement="bottom-start"
trigger="manual"
v-model="isShowSelect"
@hide="popoverHide"
>
<el-tree
ref="tree"
:data="data"
:props="defaultProps"
:show-checkbox="multiple"
@node-click="handleNodeClick"
@check-change="handleCheckChange"
></el-tree>
<el-select
slot="reference"
ref="select"
v-model="selectedData"
:multiple="multiple"
@click.native="isShowSelect = !isShowSelect"
@remove-tag="removeSelectedNodes"
@clear="removeSelectedNode"
@change="changeSelectedNodes"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-popover>
</div>
</template>
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
首先大佬利用 el-popover
弹出框为基础, 将 el-select
作为 reference
的 slot
触发 Popover
显示的 HTML 元素, el-tree
作为内容. 并且利用 select
的 option
来解决 value
和 label
的转化(虽然自己也可以解决, 但既然组件已经有轮子了, 那就拿过来用即可)
一开始我也感觉应该是这样的, 查看重构前的代码发现了另一种思路:
<el-select ref="select" v-model="selectData">
<el-option value=""></el-option>
<tree
:data="data"
:isAllOpen="false"
:isShowCheck="true"
:isMultiple="true"
:isAllChecked="false"
></tree>
</el-select>
2
3
4
5
6
7
8
9
10
.el-select-dropdown__item {
display: none;
}
2
3
给 el-option
一个默认值, 并利用 css 将其隐藏, 这样下方的 tree
组件就相当于 el-option
了
那咱们就把两者结合一下
# 初稿
<template>
<div class="select-tree">
<el-select
v-model="selectData"
:multiple="multiple"
:disabled="disabled"
:value-key="valueKey"
:size="size"
:clearable="clearable"
:collapse-tags="collapseTags"
:multiple-limit="multipleLimit"
:placeholder="placeholder"
@change="selectChange"
@remove-tag="removeTag"
>
<el-option
v-for="item in selectOptions"
:key="item.value"
:value="item.value"
:label="item.label"
></el-option>
<el-tree
ref="tree"
:data="data"
:node-key="nodeKey"
:props="props"
:highlight-current="highlightCurrent"
:default-expand-all="defaultExpandAll"
:expand-on-click-node="expandOnClickNode"
:check-on-click-node="checkOnClickNode"
:auto-expand-parent="autoExpandParent"
:default-expanded-keys="defaultExpandedKeys"
:show-checkbox="multiple"
:check-strictly="checkStrictly"
:default-checked-keys="defaultCheckedKeys"
:current-node-key="currentNodeKey"
:accordion="accordion"
:indent="indent"
@node-click="handleNodeClick"
@check-change="handleCheckChange"
></el-tree>
</el-select>
</div>
</template>
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
咱封装组件一般都喜欢保留原组件的 Attributes
, 这样别人使用的时候按照原来的习惯使用即可. 但这样就有问题了: 首先是在组件上添加了太多的 Attributes
, props
也需要都指定一下, 有时候还需要指定默认值, 工作量太大了, 而且用户实际上只使用几个属性. 理想情况下应该是: 咱们不对用户传入的属性做处理, 直接仍给原组件去处理
查看 Vue
官方文档后发现可以传入一个对象的所有属性 (opens new window), 那这样我不是接收两个属性就可以了吗? 一个是 select
的, 一个是 tree
的
# 优化 Attributes
<template>
<div class="select-tree">
<el-select
ref="select"
v-model="selectData"
:clearable="false"
v-bind="selectProps"
@visible-change="handleVisibleChange"
@remove-tag="handleRemoveTag"
@clear="handleClear"
>
<el-option
v-for="item in selectOptions"
:key="item.value"
:value="item.value"
:label="item.label"
></el-option>
<el-tree
:key="treeKey"
ref="tree"
v-bind="treeBind"
@node-click="handleNodeClick"
@current-change="handleCurrentChange"
@check-change="handleCheckChange"
></el-tree>
</el-select>
</div>
</template>
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
export default {
name: 'SelectTree',
props: {
value: {
type: [String, Number, Array],
required: true
},
selectProps: {
type: Object,
default() {
return {}
},
required: true
},
treeProps: {
type: Object,
default() {
return {}
},
required: true
},
// 单选时是否只能选择叶子节点
currentIsLeaf: {
type: Boolean,
default: false
}
},
data() {
return {
treeKey: Math.random(),
multiple: false,
selectData: '',
selectOptions: []
}
},
computed: {
treeBind() {
return {
showCheckbox: this.selectProps.multiple,
highlightCurrent: !this.selectProps.multiple,
expandOnClickNode: this.expandOnClickNode,
...this.treeProps,
defaultCheckedKeys: this.selectProps.multiple ? this.value : [],
currentNodeKey: this.selectProps.multiple ? '' : this.value
}
},
expandOnClickNode() {
return this.multiple ? true : this.currentIsLeaf ? true : 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
传入的参数:
selectProps: {
multiple: true,
// 'collapse-tags': true,
collapseTags: true,
clearable: true
},
treeProps: {
data,
showCheckbox: false,
// expandOnClickNode: false,
props: { children: 'childrenList', label: 'menuName' },
nodeKey: 'menuId'
}
2
3
4
5
6
7
8
9
10
11
12
13
使用 v-bind
是不是一下就少了好多代码, 而且也不用自己处理属性了, 完全把用户传过来的直接扔给了组件
细心观察的同学会发现上面的代码其实有对比参照的.
先看 select
的属性:
:clearable="false"
v-bind="selectProps"
2
本来是想定义一些默认属性, 然后用户的属性可以覆盖
我以为属性这样写, 会和对象一样, 下面的属性覆盖上面的, 却发现无法覆盖, 而且无论 :clearable
放在 v-bind
上面还是下面, 最终都是 false
, 貌似 v-bind
中的 clearable
没有生效
那就只能先处理一下, 再使用 v-bind
绑定. 最后成果就是 tree
中的 v-bind="treeBind"
.
但这样也有一个问题: 那就是用户只能传入驼峰值, 不能传入中划线分割的值, 否则无法覆盖默认值(注: 比较过两种传值, Vue
推荐使用中划线方式, 但现在用户其实传入的是对象, 中划线方式还需要外层包围引号, 而且对象中写中划线感觉很别扭, 就定为驼峰了)
注: 如果看过本人对 tree
的封装文章的同学, 这里可能会问为什么不用 v-bind="$attrs"
, 一是本人封装 tree
的时候才算了解这个属性; 二是这里有两个组件, 虽然目前它们需要的属性名都不一样, 但不知道以后会不会有同名但作用不同的属性, 而且传一堆没用的属性过去也不好, 所以就不改了
# 处理逻辑
主要考虑的逻辑就是 tree
的单选和多选
@current-change="handleCurrentChange"
@check-change="handleCheckChange"
// 单选,节点被点击时的回调,返回被点击的节点数据
handleCurrentChange() {
// 如果是多选就返回
if (this.multiple) return
// 获取当前选中的数据. 注: 是 data, 而不是 node
const currentNode = this.$refs.tree.getCurrentNode()
// 这个才是 node
const node = this.$refs.tree.getNode(currentNode)
// 判断是否只能选择叶子节点
if (this.currentIsLeaf && !node.isLeaf) return
this.selectOptions = []
this.selectData = ''
const value = node.key
const label = node.label
this.selectOptions.push({
value,
label
})
this.selectData = value
this.$refs.select.blur()
},
// 多选,节点勾选状态发生变化时的回调
handleCheckChange() {
// 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
this.selectOptions = [{}]
this.selectData = []
// 获取当前选中的数据. 注: 是 data, 而不是 node
const checkedNodes = this.$refs.tree.getCheckedNodes()
checkedNodes.forEach(node => {
const value = node[this.$refs.tree.nodeKey]
this.selectOptions.push({
value,
label: node[this.$refs.tree.props.label]
})
this.selectData.push(value)
})
}
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
具体逻辑上面注释已经很清楚了, 就不多做说明了
这里先吐槽一下, 明明是 getCurrentNode
和 getCheckedNode
, 返回的却是 data
. 这个应该是历史遗留问题吧, 就不深究了. 幸好官方提供了 getNode
这个方法
接下来说正事. 想说的是在取数据方面又是一个对比: 单选步骤是: 先拿到当前 data
, 再通过 data
拿到当前 node
, 不论你传入的 nodeKey
和 props.label
是什么, node
都会转化为固定的 key
和 label
; 多选的步骤是拿到当前 data
, 然后直接通过 this.$refs.tree
获取 nodeKey
和 props.label
, 再通过 data
取值
这里说一个小插曲, 本人看到 this.$refs.tree
使用很多次, 就想着能不能在初始化的时候提取出来, 这样以后就可以直接用了. 考虑过 tree
的 set
方法是否能生效, 测试了一下发现可以成功, 最后发现还是太年轻, 由于数据刷新, 使用初始化提取出来的仍然是旧值, 没有实时更新, 所以还是老老实实用 this.$refs.tree
吧
# 完成(其实是一稿, 大家都懂得)
<template>
<div class="select-tree">
<el-select
ref="select"
v-model="selectData"
v-bind="selectProps"
@visible-change="handleVisibleChange"
@remove-tag="handleRemoveTag"
@clear="handleClear"
>
<el-option
v-for="item in selectOptions"
:key="item.value"
:value="item.value"
:label="item.label"
></el-option>
<el-tree
:key="treeKey"
ref="tree"
v-bind="treeBind"
@current-change="handleCurrentChange"
@check-change="handleCheckChange"
></el-tree>
</el-select>
</div>
</template>
<script>
export default {
name: 'SelectTree',
props: {
value: {
type: [String, Number, Array],
required: true
},
selectProps: {
type: Object,
default() {
return {}
},
required: true
},
treeProps: {
type: Object,
default() {
return {}
},
required: true
},
// 单选时是否只能选择叶子节点
currentIsLeaf: {
type: Boolean,
default: false
}
},
data() {
return {
treeKey: Math.random(),
selectData: '',
selectOptions: []
}
},
computed: {
treeBind() {
return {
showCheckbox: this.selectProps.multiple,
highlightCurrent: !this.selectProps.multiple,
expandOnClickNode: this.expandOnClickNode,
...this.treeProps,
defaultCheckedKeys: this.selectProps.multiple ? this.value : [],
currentNodeKey: this.selectProps.multiple ? '' : this.value
}
},
multiple() {
return this.selectProps.multiple
},
expandOnClickNode() {
return this.multiple ? true : this.currentIsLeaf ? true : false
}
},
watch: {
value() {
// 为了检测 v-model 的变化
if (this.value + '' !== this.selectData + '') {
this.treeKey = Math.random()
this.init()
}
}
},
methods: {
init() {
this.$nextTick(() => {
if (this.multiple) {
this.handleCheckChange()
} else {
this.handleCurrentChange()
}
})
},
// select 下拉框出现/隐藏
handleVisibleChange(val) {
// 下拉框隐藏并且值改变后
if (!val && this.value + '' !== this.selectData + '') {
this.$emit('input', this.selectData)
this.$emit('change', this.selectData)
}
this.$emit('visible-change', val)
},
// select 清空
handleClear() {
if (this.$refs.tree.showCheckbox) {
this.selectData = []
this.$refs.tree.setCheckedKeys([])
} else {
this.selectData = ''
this.$refs.tree.setCurrentKey(null)
}
this.$emit('input', this.selectData)
this.$emit('change', this.selectData)
this.$emit('clear')
},
// select 移除 tag
handleRemoveTag(val) {
this.$refs.tree.setChecked(val, false)
let node = this.$refs.tree.getNode(val)
if (!this.$refs.tree.checkStrictly && node.childNodes.length > 0) {
this.tree2List(node).map(item => {
if (item.childNodes.length <= 0) {
this.$refs.tree.setChecked(item, false)
}
})
this.handleCheckChange()
}
this.$emit('input', this.selectData)
this.$emit('change', this.selectData)
this.$emit('remove-tag', val)
},
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// 如果多选, 不处理
if (this.multiple) return
// 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
this.selectOptions = [{}]
const currentNode = this.$refs.tree.getCurrentNode()
// 初始值为空
if (!currentNode) return
const node = this.$refs.tree.getNode(currentNode)
// 判断叶子节点
if (this.currentIsLeaf && !node.isLeaf) return
this.selectOptions = []
this.selectData = ''
const value = node.key
const label = node.label
this.selectOptions.push({
value,
label
})
this.selectData = value
this.$refs.select.blur()
},
// 多选,节点勾选状态发生变化时的回调
handleCheckChange() {
// 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
this.selectOptions = [{}]
this.selectData = []
const checkedNodes = this.$refs.tree.getCheckedNodes()
checkedNodes.forEach(node => {
const value = node[this.$refs.tree.nodeKey]
this.selectOptions.push({
value,
label: node[this.$refs.tree.props.label]
})
this.selectData.push(value)
})
},
tree2List(tree) {
let queen = []
let out = []
queen = queen.concat(tree)
while (queen.length) {
let first = queen.shift()
if (first.childNodes) {
queen = queen.concat(first.childNodes)
}
out.push(first)
}
return out
}
},
mounted() {
this.init()
}
}
</script>
<style lang="less" scoped>
.select-tree {
display: inline-block;
width: 100%;
}
.el-select-dropdown__item {
display: none;
}
</style>
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# 修正
经后期测试, 通过 this.$refs.tree
获取 nodeKey
和 props.label
存在一定问题, 如果用户未传入 nodeKey
或 props.label
, 那么拿到的值都是 undefined
, 这种方式还是不靠谱, 还是用单选方式靠谱. 当时本人测试的时候这两个值都传入了, 所以没有测出 bug
, 实际使用的时候, 没有传入 nodeKey
, 导致获取到的值都是 undefined
so, 咱们改成单选方式试试:
// 多选,节点勾选状态发生变化时的回调
handleCheckChange() {
// 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
this.selectOptions = [{}]
this.selectData = []
const checkedNodes = this.$refs.tree.getCheckedNodes()
checkedNodes.forEach(node => {
const checkedNode = this.$refs.tree.getNode(node)
const value = checkedNode.key
this.selectOptions.push({
value,
label: checkedNode.label
})
this.selectData.push(value)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用以上代码发现 getNode
永远返回 null
, 原因是 nodeKey
为 undefined
, 无法获取 node
. 而且 getCheckedNodes
可以正常返回数据, getCheckedKeys
返回的都是 undefined
. 所以必须给 nodeKey
一个默认值了.
treeBind() {
return {
showCheckbox: this.selectProps.multiple,
highlightCurrent: !this.selectProps.multiple,
expandOnClickNode: this.expandOnClickNode,
nodeKey: 'id',
...this.treeProps,
defaultCheckedKeys: this.selectProps.multiple ? this.value : [],
currentNodeKey: this.selectProps.multiple ? '' : this.value
}
}
2
3
4
5
6
7
8
9
10
11
这样, getCheckedNodes
, getCheckedKeys
, getNode
都可以正常使用了
# 实际工作中发现的问题
# tree 为 undefined(已解决)
由于本人后面封装了 tree
, 就想着使用 tree
代替这里的 el-tree
, 并且通过懒加载方式引入组件
components: {
Tree: resolve => require(['plugins/Tree'], resolve)
}
2
3
结果杯具了, this.$refs.tree
初始化永远是 undefined
, 只有手动点击下拉框后才能正常获取.
一开始以为加一个 $nextTick
就好了, 最后发现没用. 然后就想到是不是懒加载影响的, 替换成 import Tree from 'plugins/Tree'
一下就好了. 看来什么东西也不能太绝对了, 太追求性能也不行
# EditableElements 使用 select-tree 时, placeholder 不会自动生成(已解决)
如果不知道 EditableElements
, 可以看看这篇文章: 基于 ElementUI 封装的基础 table 和 form | Henry
原因是现在只能通过 selectProps
来传入 placeholder
, 直接传入的话是不认的
其实这里设计的有点问题, tree
的 props
必须通过 treeProps
传入, select
的 props
应该是直接传入即可, 就和 el-select
一样
那就兼容一下吧:
<el-select
v-bind="{ ...$attrs, ...selectProps }"
>
2
3
props: {
multiple: {
type: Boolean,
default: false
}
},
computed: {
treeBind() {
return {
showCheckbox: this.isMultiple,
highlightCurrent: !this.isMultiple,
expandOnClickNode: this.expandOnClickNode,
nodeKey: 'id',
...this.treeProps,
defaultCheckedKeys: this.isMultiple ? this.value : [],
currentNodeKey: this.isMultiple ? '' : this.value
}
},
isMultiple() {
return this.selectProps.multiple || this.multiple
},
expandOnClickNode() {
return this.isMultiple ? true : this.currentIsLeaf
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
this.multiple
修改为 this.isMultiple
即可
这样就可以
由于本组件已经在项目中使用了, 去掉 selectProps
改动有点大, 只能这样兼容一下
如果是新项目, 可以去掉 selectProps
了:
<el-select
v-bind="$attrs"
>
2
3
# 单选时只能选择叶子节点, 但会选择父级(已解决)(改为 Tree 解决)
其实这个应该是 Tree
的功能, 当初未想到这些, 导致加到了这里. 但思路是相通的
基于 ElementUI 封装的 Tree2 | Henry
目前只是通过 node.isLeaf
来判断是否是叶子节点, 但有时候只有一个父级, 那么这个父级既是父节点, 又是叶子节点, 我们有时候是不能选择这个节点的. 当然如果你们的需求就是可以选择这个节点, 那就不用往下看了, 目前完全可以胜任你们的需求
而且判断是不是叶子节点, 不同的数据源会有不同的判断方式, 所以需要放出一个方法, 让使用者自己判断是否是叶子节点
添加一个 props
:
props: {
/**
* @description: 自定义单选时只能选择子节点方法; 优先级高于 currentIsLeaf
* @param {data: Object}: 当前节点数据
* @param {node: Object}: 当前节点 Node 对象
* @return: Boolean
*/
isLeafFun: {
type: Function
}
},
methods: {
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// do something
// 判断叶子节点
if (this.isLeafFun ? this.isLeafFun(currentNode, node) : !node.isLeaf && this.currentIsLeaf) {
return
}
// do something
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 单选时只能选择叶子节点, 但点击父节点后, 父节点会有选中状态(已解决)
比如当前选中一个叶子节点, 但需要选择另一个叶子节点, 当点击这个叶子节点的父节点时, 初始选中的叶子节点的选中状态消失了, 出现在父节点上了; 虽然不会改变选择框中的值, 但也算一个显示 bug
所以我们需要在判断是否是叶子节点中做一些处理: 如果不是叶子节点, 需要设置当前选中为上一个已选中的叶子节点或 null
(没有上一个已选中的叶子节点)
// 判断叶子节点
if (this.isLeafFun ? this.isLeafFun(currentNode, node) : !node.isLeaf && this.currentIsLeaf) {
// 如果不是叶子节点, 设置当前选中节点仍为上一个叶子节点
this.$refs.tree.setCurrentKey(this.selectData || null)
return
}
2
3
4
5
6
# v-model 绑定的值赋值为空后, 界面仍显示上次的值(已解决)
主要问题出现在这里:
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// do something
const currentNode = this.$refs.tree.getCurrentNode()
// 初始值为空
if (!currentNode) return
// do something
}
2
3
4
5
6
7
8
这里由于 v-model
传入的是一个空值, 在 tree
中无法找到(其实不只是空值, 只要是在 tree
中无法找到就有这个问题), 从而直接返回了, 并没有对 selectData
做清空操作, 导致仍显示上次的结果
所以我们需要在这里清空一下 selectData
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// do something
const currentNode = this.$refs.tree.getCurrentNode()
// 当前传入的值在 tree 中无法找到, 需要清空 select 值
if (!currentNode) {
this.selectData = ''
return
}
// do something
}
2
3
4
5
6
7
8
9
10
11
可能有小伙伴问了: 你下面也有清空操作, 这里也有, 那直接把清空操作提出来, 在这里清空不就好了吗? 下面就不用写了呀
但这里不能将清空操作提出来, 因为下面还有一个判断叶子节点的逻辑. 如果在这里清空了, 下面进入判断叶子节点的逻辑后, 直接返回了: 这里正常逻辑是维持界面显示不变, 但由于上面清空了, 导致与实际情况不符, 所以不能提出来
# 在 el-form 必填验证时, 第一次选择仍提示为必填(已解决)
如上图所示, 第一次选择后, 仍提示为必填, 只有第二次选择后才正常, 就好像每次验证的都是上次的结果
被这个问题困扰了好久, 最近终于知道为啥了
简略代码如下:
methods: {
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// do something
// 当前传入的值在 tree 中无法找到, 需要清空 select 值
if (!currentNode) {
this.selectData = ''
this.selectNode = null
return
}
// do something
this.selectData = value
this.selectNode = node.data
console.log('blur')
this.$refs.select.blur()
},
// select 下拉框出现/隐藏
handleVisibleChange(val) {
// 如果有过滤, 下拉框出现后, 重置搜索
if (val && this.filterable) {
this.filter()
}
// 下拉框隐藏并且值改变后
if (!val && this.value + '' !== this.selectData + '') {
this.$emit('input', this.selectData)
this.$emit('change', this.selectData, this.selectNode)
console.log('emit')
}
this.$emit('visible-change', val)
}
}
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
输出:
由以上代码和图片可以看出, 当单选选完后, 调用 el-select
的 blur
方法, 此时其实已经触发验证了, 而我们 emit
此时才调用, 所以此时验证的是上一次的值, 所以第一次会提示必填
所以我们得在 blur
方法之前调用 emit
才可以
methods: {
emitBase() {
this.$emit('input', this.selectData)
this.$emit('change', this.selectData, this.selectNode)
},
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// do something
// 当前传入的值在 tree 中无法找到, 需要清空 select 值
if (!currentNode) {
this.selectData = ''
this.selectNode = null
this.emitBase()
this.$refs.select.blur()
return
}
// do something
this.selectData = value
this.selectNode = node.data
this.emitBase()
console.log('blur')
this.$refs.select.blur()
},
// select 下拉框出现/隐藏
handleVisibleChange(val) {
// 如果有过滤, 下拉框出现后, 重置搜索
if (val && this.filterable) {
this.filter()
}
// 下拉框隐藏并且值改变后
if (!val && this.value + '' !== this.selectData + '') {
this.emitBase()
console.log('emit')
}
console.log('visible', val)
this.$emit('visible-change', val)
}
}
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
结果:
输出:
由于在 blur
之前就 emit
了, 所以在 handleVisibleChange
中 this.value
和 this.selectData
是相同的, 所以没有输出 emit
但这样又出现新的问题: 初始化的时候就会调用 handleCurrentChange
, 从而 emit change
, 所以我们需要一个变量来标记一下是否是第一次
data: {
isFirst: true
},
methods: {
// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
// do something
// 当前传入的值在 tree 中无法找到, 需要清空 select 值
if (!currentNode) {
this.selectData = ''
this.selectNode = null
if (this.isFirst) {
this.isFirst = false
} else {
this.emitBase()
this.$refs.select.blur()
}
return
}
// do something
this.selectData = value
this.selectNode = node.data
if (this.isFirst) {
this.isFirst = false
} else {
this.emitBase()
console.log('blur')
this.$refs.select.blur()
}
}
}
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
单选的问题解决了, 多选也需要解决一下:
但在多选的 handleCheckChange
中不能直接调用 emitBase
, 因为会把 change
也 emit
出去, 但其实只有下拉框关闭(也就是 blur
后才能 emit change
), 所以这里只 emit input
// 多选, 节点勾选状态发生变化时的回调
handleCheckChange() {
// do something
if (this.isFirst) {
this.isFirst = false
} else {
this.$emit('input', this.selectData)
}
}
2
3
4
5
6
7
8
9
但这里 emit input
后, 在 handleVisibleChange
中 this.value
和 this.selectData
是相同的, 这样就不会调用 emitBase
了, 无法 emit change
了; 所以对于多选, 还需要一个变量: 在下拉框打开时保存 this.value
, 在关闭后, 判断该变量与 this.value
是否相同
// select 下拉框出现/隐藏
handleVisibleChange(val) {
// 下拉框出现并且是多选, 将 this.value 保存到变量 multipleTempValue
if (val && this.multiple) {
this.multipleTempValue = deepClone(this.value)
}
// 如果有过滤, 下拉框出现后, 重置搜索
if (val && this.filterable) {
this.filter()
}
// 下拉框隐藏并且是多选, 判断值是否变化
if (!val && this.multiple && this.value + '' !== this.multipleTempValue + '') {
this.$emit('change', this.selectData, this.selectNode)
}
this.$emit('visible-change', val)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 在 el-form 必填验证时, 选择一项后再调用 form 重置功能仍提示为必填(已解决)
经过简单调试后, 发现是 $nextTick
的原因, 但又必须要用这个方法, 因为使用 key
重新加载 Tree
后, 只有在 $nextTick
后 Tree
才加载完成, 才可以使用 Tree
的方法
那有没有不使用 $nextTick
也能知道 Tree
加载完成, 并可以使用 Tree
的方法呢?
答案是肯定的, 那就是 @hook
: 外部监听生命周期函数
我们在 Tree
中监听 @hook:mounted
并执行 init
就可以了吧
<tree
:key="treeKey"
ref="tree"
v-bind="treeBind"
@current-change="handleCurrentChange"
@check-change="handleCheckChange"
@hook:mounted="init"
></tree>
<script>
export default {
watch: {
value() {
console.log(this.value, 'value')
// 为了检测 v-model 的变化
if (this.value + '' !== this.selectData + '') {
console.log('value change')
this.treeKey = Math.random()
// this.init()
}
}
},
methods: {
init() {
// this.$nextTick(() => {
if (this.isMultiple) {
this.handleCheckChange()
} else {
this.handleCurrentChange()
}
// })
}
},
mounted() {
// this.init()
// 绑定 el-select 方法
for (let key in this.$refs.select) {
if (!(key in this) && typeof this.$refs.select[key] === 'function') {
this[key] = this.$refs.select[key].bind(this.$refs.select)
}
}
}
}
</script>
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
这样确实是可以了, 但会报错:
this.$refs.tree.getCurrentNode is not a function
原因是我们在 mounted
中给 Tree
添加方法的, @hook:mounted
时还没有方法, 所以报错
那就简单了, 咱们不用 hook
了, 等方法全部添加后, emit
一个事件即可
Tree.vue
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])
}
}
this.$emit('ready')
}
2
3
4
5
6
7
8
9
# 在 el-form 必填验证时, 多选初始化就提示必填
- 01
- 搭配 Jenkins 实现自动化打包微前端多个项目09-15
- 02
- 自动化打包微前端多个项目09-15
- 03
- el-upload 直传阿里 oss 并且显示自带进度条和视频回显封面图06-05