Skip to content

Commit 4fe9c2b

Browse files
authored
fix(components): [TreeSelect] checkbox interaction (element-plus#8102)
* fix(components): [TreeSelect] check child when parent checked * refactor(components): [TreeSelect] move utility functions to utils.ts * fix(components): [TreeSelect] exclude check event from click node * fix(components): [TreeSelect] handle dup when `checkOnClickNode` * feat(components): [TreeSelect] `treeFind` support find parent node * fix(components): [TreeSelect] show current selected node only first time * fix(components): [TreeSelect] incorrect node selection condition * docs(components): [TreeSelect] update checkbox examples and tips * fix(components): [TreeSelect] incorrect label when data modify
1 parent ede25ea commit 4fe9c2b

File tree

8 files changed

+279
-43
lines changed

8 files changed

+279
-43
lines changed

docs/en-US/component/tree-select.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ tree-select/basic
1818

1919
:::
2020

21+
:::tip
22+
23+
Since `render-after-expand` defaults to false,
24+
the selected label name may not be displayed when echoing,
25+
you can set it to true to display the correct name.
26+
27+
:::
28+
2129
## Select any level
2230

2331
When using the `check-strictly=true` attribute, any node can be checked,
@@ -29,6 +37,14 @@ tree-select/check-strictly
2937

3038
:::
3139

40+
:::tip
41+
42+
When using `show-checkbox`, since `check-on-click-node` is false by default,
43+
it can only be selected by checking, you can set it to true,
44+
and then click the node to select.
45+
46+
:::
47+
3248
## Multiple Selection
3349

3450
Multiple selection using clicks or checkbox.

docs/examples/tree-select/basic.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<template>
2-
<el-tree-select v-model="value" :data="data" />
2+
<el-tree-select v-model="value" :data="data" :render-after-expand="false" />
3+
<el-divider />
4+
show checkbox:
5+
<el-tree-select
6+
v-model="value"
7+
:data="data"
8+
:render-after-expand="false"
9+
show-checkbox
10+
/>
311
</template>
412

513
<script lang="ts" setup>

docs/examples/tree-select/check-strictly.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
<template>
2-
<el-tree-select v-model="value" :data="data" check-strictly />
2+
<el-tree-select
3+
v-model="value"
4+
:data="data"
5+
check-strictly
6+
:render-after-expand="false"
7+
/>
8+
<el-divider />
9+
show checkbox(only click checkbox to select):
10+
<el-tree-select
11+
v-model="value"
12+
:data="data"
13+
check-strictly
14+
:render-after-expand="false"
15+
show-checkbox
16+
/>
17+
<el-divider />
18+
show checkbox with `check-on-click-node`:
19+
<el-tree-select
20+
v-model="value"
21+
:data="data"
22+
check-strictly
23+
:render-after-expand="false"
24+
show-checkbox
25+
check-on-click-node
26+
/>
327
</template>
428

529
<script lang="ts" setup>

docs/examples/tree-select/multiple.vue

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
<template>
2-
<el-tree-select v-model="value" :data="data" multiple />
2+
<el-tree-select
3+
v-model="value"
4+
:data="data"
5+
multiple
6+
:render-after-expand="false"
7+
/>
38
<el-divider />
49
show checkbox:
5-
<el-tree-select v-model="value" :data="data" multiple show-checkbox />
10+
<el-tree-select
11+
v-model="value"
12+
:data="data"
13+
multiple
14+
:render-after-expand="false"
15+
show-checkbox
16+
/>
17+
<el-divider />
18+
show checkbox with `check-strictly`:
19+
<el-tree-select
20+
v-model="valueStrictly"
21+
:data="data"
22+
multiple
23+
:render-after-expand="false"
24+
show-checkbox
25+
check-strictly
26+
check-on-click-node
27+
/>
628
</template>
729

830
<script lang="ts" setup>
931
import { ref } from 'vue'
1032
1133
const value = ref()
34+
const valueStrictly = ref()
1235
1336
const data = [
1437
{

packages/components/tree-select/__tests__/tree-select.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,59 @@ describe('TreeSelect.vue', () => {
361361
expect(select.vm.modelValue).toEqual([])
362362
expect(wrapperRef.getCheckedKeys()).toEqual([])
363363
})
364+
365+
test('only show checkbox', async () => {
366+
const { select, tree } = createComponent({
367+
props: {
368+
showCheckbox: true,
369+
},
370+
})
371+
372+
// check child node when folder node checked,
373+
// value.value will be 111
374+
await tree
375+
.find('.el-tree-node__content .el-checkbox__original')
376+
.trigger('click')
377+
await nextTick()
378+
expect(select.vm.modelValue).equal(111)
379+
380+
// unselect when has child checked
381+
await tree
382+
.find('.el-tree-node__content .el-checkbox__original')
383+
.trigger('click')
384+
await nextTick()
385+
expect(select.vm.modelValue).toBe(undefined)
386+
})
387+
388+
test('show checkbox and check on click node', async () => {
389+
const { select, tree } = createComponent({
390+
props: {
391+
showCheckbox: true,
392+
checkOnClickNode: true,
393+
},
394+
})
395+
396+
// check child node when folder node checked,
397+
// value.value will be 111
398+
await tree.findAll('.el-tree-node__content').slice(-1)[0].trigger('click')
399+
await nextTick()
400+
expect(select.vm.modelValue).equal(111)
401+
402+
// unselect when has child checked
403+
await tree.findAll('.el-tree-node__content').slice(-1)[0].trigger('click')
404+
await nextTick()
405+
expect(select.vm.modelValue).toBe(undefined)
406+
})
407+
408+
test('expand selected node`s parent in first time', async () => {
409+
const value = ref(111)
410+
const { tree } = createComponent({
411+
props: {
412+
modelValue: value,
413+
},
414+
})
415+
416+
expect(tree.findAll('.is-expanded[data-key="1"]').length).toBe(1)
417+
expect(tree.findAll('.is-expanded[data-key="11"]').length).toBe(1)
418+
})
364419
})

packages/components/tree-select/src/tree-select-option.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineComponent } from 'vue'
1+
import { defineComponent, getCurrentInstance, nextTick } from 'vue'
22
import { ElOption } from '@element-plus/components/select'
33

44
const component = defineComponent({
@@ -9,6 +9,23 @@ const component = defineComponent({
99
// use methods.selectOptionClick
1010
delete result.selectOptionClick
1111

12+
const vm = (getCurrentInstance() as NonNullable<any>).proxy
13+
14+
// Fix: https://github.com/element-plus/element-plus/issues/7917
15+
// `el-option` will delete the cache before unmount,
16+
// This is normal for flat arrays `<el-select><el-option v-for="3"></el-select>`,
17+
// Because the same node key does not create a difference node,
18+
// But in tree data, the same key at different levels will create diff nodes,
19+
// So the destruction of `el-option` in `nextTick` will be slower than
20+
// the creation of new `el-option`, which will delete the new node,
21+
// here restore the deleted node.
22+
// @link https://github.com/element-plus/element-plus/blob/6df6e49db07b38d6cc3b5e9a960782bd30879c11/packages/components/select/src/option.vue#L78
23+
nextTick(() => {
24+
if (!result.select.cachedOptions.get(vm.value)) {
25+
result.select.onOptionCreate(vm)
26+
}
27+
})
28+
1229
return result
1330
},
1431
methods: {

packages/components/tree-select/src/tree.ts

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
55
import { isFunction } from '@element-plus/utils'
66
import ElTree from '@element-plus/components/tree'
77
import TreeSelectOption from './tree-select-option'
8+
import { isValidArray, isValidValue, toValidArray, treeFind } from './utils'
89
import type { Ref } from 'vue'
910
import type ElSelect from '@element-plus/components/select'
1011
import type Node from '@element-plus/components/tree/src/model/node'
@@ -67,6 +68,18 @@ export const useTree = (
6768
}
6869
}
6970

71+
const defaultExpandedParentKeys = toValidArray(props.modelValue)
72+
.map((value) => {
73+
return treeFind(
74+
props.data || [],
75+
(data) => getNodeValByProp('value', data) === value,
76+
(data) => getNodeValByProp('children', data),
77+
(data, index, array, parent) =>
78+
parent && getNodeValByProp('value', parent)
79+
)
80+
})
81+
.filter((item) => isValidValue(item))
82+
7083
return {
7184
...pick(toRefs(props), Object.keys(ElTree.props)),
7285
...attrs,
@@ -77,17 +90,12 @@ export const useTree = (
7790
return !props.checkStrictly
7891
}),
7992

80-
// auto expand selected parent node
93+
// show current selected node only first time,
94+
// fix the problem of expanding multiple nodes when checking multiple nodes
8195
defaultExpandedKeys: computed(() => {
82-
const values = toValidArray(props.modelValue)
83-
const parentKeys = tree.value
84-
? values
85-
.map((item) => tree.value?.getNode(item)?.parent?.key)
86-
.filter((item) => isValidValue(item))
87-
: values
8896
return props.defaultExpandedKeys
89-
? props.defaultExpandedKeys.concat(parentKeys)
90-
: parentKeys
97+
? props.defaultExpandedKeys.concat(defaultExpandedParentKeys)
98+
: defaultExpandedParentKeys
9199
}),
92100

93101
renderContent: (h, { node, data, store }) => {
@@ -114,14 +122,11 @@ export const useTree = (
114122
onNodeClick: (data, node, e) => {
115123
attrs.onNodeClick?.(data, node, e)
116124

117-
if (
118-
(props.checkStrictly
119-
? props.showCheckbox
120-
? props.checkOnClickNode
121-
: props.checkStrictly
122-
: props.checkStrictly) ||
123-
node.isLeaf
124-
) {
125+
// `onCheck` is trigger when `checkOnClickNode`
126+
if (props.showCheckbox && props.checkOnClickNode) return
127+
128+
// now `checkOnClickNode` is false, only no checkbox and `checkStrictly` or `isLeaf`
129+
if (!props.showCheckbox && (props.checkStrictly || node.isLeaf)) {
125130
if (!getNodeValByProp('disabled', data)) {
126131
const option = select.value?.options.get(
127132
getNodeValByProp('value', data)
@@ -135,28 +140,55 @@ export const useTree = (
135140
onCheck: (data, params) => {
136141
attrs.onCheck?.(data, params)
137142

138-
// remove folder node when `checkStrictly` is false
139-
const checkedKeys = !props.checkStrictly
140-
? tree.value?.getCheckedKeys(true)
141-
: params.checkedKeys
143+
const dataValue = getNodeValByProp('value', data)
144+
if (props.checkStrictly) {
145+
emit(
146+
UPDATE_MODEL_EVENT,
147+
// Checking for changes may come from `check-on-node-click`
148+
props.multiple
149+
? params.checkedKeys
150+
: params.checkedKeys.includes(dataValue)
151+
? dataValue
152+
: undefined
153+
)
154+
}
155+
// only can select leaf node
156+
else {
157+
if (props.multiple) {
158+
emit(
159+
UPDATE_MODEL_EVENT,
160+
(tree.value as InstanceType<typeof ElTree>).getCheckedKeys(true)
161+
)
162+
} else {
163+
// select first leaf node when check parent
164+
const firstLeaf = treeFind(
165+
[data],
166+
(data) =>
167+
!isValidArray(getNodeValByProp('children', data)) &&
168+
!getNodeValByProp('disabled', data),
169+
(data) => getNodeValByProp('children', data)
170+
)
171+
const firstLeafKey = firstLeaf
172+
? getNodeValByProp('value', firstLeaf)
173+
: undefined
142174

143-
const value = getNodeValByProp('value', data)
144-
emit(
145-
UPDATE_MODEL_EVENT,
146-
props.multiple
147-
? checkedKeys
148-
: checkedKeys.includes(value)
149-
? value
150-
: undefined
151-
)
175+
// unselect when any child checked
176+
const hasCheckedChild =
177+
isValidValue(props.modelValue) &&
178+
!!treeFind(
179+
[data],
180+
(data) => getNodeValByProp('value', data) === props.modelValue,
181+
(data) => getNodeValByProp('children', data)
182+
)
183+
184+
emit(
185+
UPDATE_MODEL_EVENT,
186+
firstLeafKey === props.modelValue || hasCheckedChild
187+
? undefined
188+
: firstLeafKey
189+
)
190+
}
191+
}
152192
},
153193
}
154194
}
155-
156-
function isValidValue(val: any) {
157-
return val || val === 0
158-
}
159-
160-
function toValidArray(val: any) {
161-
return Array.isArray(val) ? val : isValidValue(val) ? [val] : []
162-
}

0 commit comments

Comments
 (0)