fix(components): [TreeSelect] checkbox interaction (#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
This commit is contained in:
yujinpan 2022-07-02 14:49:21 +08:00 committed by GitHub
parent ede25ea5b0
commit 4fe9c2bd6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 279 additions and 43 deletions

View File

@ -18,6 +18,14 @@ tree-select/basic
:::
:::tip
Since `render-after-expand` defaults to false,
the selected label name may not be displayed when echoing,
you can set it to true to display the correct name.
:::
## Select any level
When using the `check-strictly=true` attribute, any node can be checked,
@ -29,6 +37,14 @@ tree-select/check-strictly
:::
:::tip
When using `show-checkbox`, since `check-on-click-node` is false by default,
it can only be selected by checking, you can set it to true,
and then click the node to select.
:::
## Multiple Selection
Multiple selection using clicks or checkbox.

View File

@ -1,5 +1,13 @@
<template>
<el-tree-select v-model="value" :data="data" />
<el-tree-select v-model="value" :data="data" :render-after-expand="false" />
<el-divider />
show checkbox:
<el-tree-select
v-model="value"
:data="data"
:render-after-expand="false"
show-checkbox
/>
</template>
<script lang="ts" setup>

View File

@ -1,5 +1,29 @@
<template>
<el-tree-select v-model="value" :data="data" check-strictly />
<el-tree-select
v-model="value"
:data="data"
check-strictly
:render-after-expand="false"
/>
<el-divider />
show checkbox(only click checkbox to select):
<el-tree-select
v-model="value"
:data="data"
check-strictly
:render-after-expand="false"
show-checkbox
/>
<el-divider />
show checkbox with `check-on-click-node`:
<el-tree-select
v-model="value"
:data="data"
check-strictly
:render-after-expand="false"
show-checkbox
check-on-click-node
/>
</template>
<script lang="ts" setup>

View File

@ -1,14 +1,37 @@
<template>
<el-tree-select v-model="value" :data="data" multiple />
<el-tree-select
v-model="value"
:data="data"
multiple
:render-after-expand="false"
/>
<el-divider />
show checkbox:
<el-tree-select v-model="value" :data="data" multiple show-checkbox />
<el-tree-select
v-model="value"
:data="data"
multiple
:render-after-expand="false"
show-checkbox
/>
<el-divider />
show checkbox with `check-strictly`:
<el-tree-select
v-model="valueStrictly"
:data="data"
multiple
:render-after-expand="false"
show-checkbox
check-strictly
check-on-click-node
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const valueStrictly = ref()
const data = [
{

View File

@ -361,4 +361,59 @@ describe('TreeSelect.vue', () => {
expect(select.vm.modelValue).toEqual([])
expect(wrapperRef.getCheckedKeys()).toEqual([])
})
test('only show checkbox', async () => {
const { select, tree } = createComponent({
props: {
showCheckbox: true,
},
})
// check child node when folder node checked,
// value.value will be 111
await tree
.find('.el-tree-node__content .el-checkbox__original')
.trigger('click')
await nextTick()
expect(select.vm.modelValue).equal(111)
// unselect when has child checked
await tree
.find('.el-tree-node__content .el-checkbox__original')
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(undefined)
})
test('show checkbox and check on click node', async () => {
const { select, tree } = createComponent({
props: {
showCheckbox: true,
checkOnClickNode: true,
},
})
// check child node when folder node checked,
// value.value will be 111
await tree.findAll('.el-tree-node__content').slice(-1)[0].trigger('click')
await nextTick()
expect(select.vm.modelValue).equal(111)
// unselect when has child checked
await tree.findAll('.el-tree-node__content').slice(-1)[0].trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(undefined)
})
test('expand selected node`s parent in first time', async () => {
const value = ref(111)
const { tree } = createComponent({
props: {
modelValue: value,
},
})
expect(tree.findAll('.is-expanded[data-key="1"]').length).toBe(1)
expect(tree.findAll('.is-expanded[data-key="11"]').length).toBe(1)
})
})

View File

@ -1,4 +1,4 @@
import { defineComponent } from 'vue'
import { defineComponent, getCurrentInstance, nextTick } from 'vue'
import { ElOption } from '@element-plus/components/select'
const component = defineComponent({
@ -9,6 +9,23 @@ const component = defineComponent({
// use methods.selectOptionClick
delete result.selectOptionClick
const vm = (getCurrentInstance() as NonNullable<any>).proxy
// Fix: https://github.com/element-plus/element-plus/issues/7917
// `el-option` will delete the cache before unmount,
// This is normal for flat arrays `<el-select><el-option v-for="3"></el-select>`,
// Because the same node key does not create a difference node,
// But in tree data, the same key at different levels will create diff nodes,
// So the destruction of `el-option` in `nextTick` will be slower than
// the creation of new `el-option`, which will delete the new node,
// here restore the deleted node.
// @link https://github.com/element-plus/element-plus/blob/6df6e49db07b38d6cc3b5e9a960782bd30879c11/packages/components/select/src/option.vue#L78
nextTick(() => {
if (!result.select.cachedOptions.get(vm.value)) {
result.select.onOptionCreate(vm)
}
})
return result
},
methods: {

View File

@ -5,6 +5,7 @@ import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { isFunction } from '@element-plus/utils'
import ElTree from '@element-plus/components/tree'
import TreeSelectOption from './tree-select-option'
import { isValidArray, isValidValue, toValidArray, treeFind } from './utils'
import type { Ref } from 'vue'
import type ElSelect from '@element-plus/components/select'
import type Node from '@element-plus/components/tree/src/model/node'
@ -67,6 +68,18 @@ export const useTree = (
}
}
const defaultExpandedParentKeys = toValidArray(props.modelValue)
.map((value) => {
return treeFind(
props.data || [],
(data) => getNodeValByProp('value', data) === value,
(data) => getNodeValByProp('children', data),
(data, index, array, parent) =>
parent && getNodeValByProp('value', parent)
)
})
.filter((item) => isValidValue(item))
return {
...pick(toRefs(props), Object.keys(ElTree.props)),
...attrs,
@ -77,17 +90,12 @@ export const useTree = (
return !props.checkStrictly
}),
// auto expand selected parent node
// show current selected node only first time,
// fix the problem of expanding multiple nodes when checking multiple nodes
defaultExpandedKeys: computed(() => {
const values = toValidArray(props.modelValue)
const parentKeys = tree.value
? values
.map((item) => tree.value?.getNode(item)?.parent?.key)
.filter((item) => isValidValue(item))
: values
return props.defaultExpandedKeys
? props.defaultExpandedKeys.concat(parentKeys)
: parentKeys
? props.defaultExpandedKeys.concat(defaultExpandedParentKeys)
: defaultExpandedParentKeys
}),
renderContent: (h, { node, data, store }) => {
@ -114,14 +122,11 @@ export const useTree = (
onNodeClick: (data, node, e) => {
attrs.onNodeClick?.(data, node, e)
if (
(props.checkStrictly
? props.showCheckbox
? props.checkOnClickNode
: props.checkStrictly
: props.checkStrictly) ||
node.isLeaf
) {
// `onCheck` is trigger when `checkOnClickNode`
if (props.showCheckbox && props.checkOnClickNode) return
// now `checkOnClickNode` is false, only no checkbox and `checkStrictly` or `isLeaf`
if (!props.showCheckbox && (props.checkStrictly || node.isLeaf)) {
if (!getNodeValByProp('disabled', data)) {
const option = select.value?.options.get(
getNodeValByProp('value', data)
@ -135,28 +140,55 @@ export const useTree = (
onCheck: (data, params) => {
attrs.onCheck?.(data, params)
// remove folder node when `checkStrictly` is false
const checkedKeys = !props.checkStrictly
? tree.value?.getCheckedKeys(true)
: params.checkedKeys
const dataValue = getNodeValByProp('value', data)
if (props.checkStrictly) {
emit(
UPDATE_MODEL_EVENT,
// Checking for changes may come from `check-on-node-click`
props.multiple
? params.checkedKeys
: params.checkedKeys.includes(dataValue)
? dataValue
: undefined
)
}
// only can select leaf node
else {
if (props.multiple) {
emit(
UPDATE_MODEL_EVENT,
(tree.value as InstanceType<typeof ElTree>).getCheckedKeys(true)
)
} else {
// select first leaf node when check parent
const firstLeaf = treeFind(
[data],
(data) =>
!isValidArray(getNodeValByProp('children', data)) &&
!getNodeValByProp('disabled', data),
(data) => getNodeValByProp('children', data)
)
const firstLeafKey = firstLeaf
? getNodeValByProp('value', firstLeaf)
: undefined
const value = getNodeValByProp('value', data)
emit(
UPDATE_MODEL_EVENT,
props.multiple
? checkedKeys
: checkedKeys.includes(value)
? value
: undefined
)
// unselect when any child checked
const hasCheckedChild =
isValidValue(props.modelValue) &&
!!treeFind(
[data],
(data) => getNodeValByProp('value', data) === props.modelValue,
(data) => getNodeValByProp('children', data)
)
emit(
UPDATE_MODEL_EVENT,
firstLeafKey === props.modelValue || hasCheckedChild
? undefined
: firstLeafKey
)
}
}
},
}
}
function isValidValue(val: any) {
return val || val === 0
}
function toValidArray(val: any) {
return Array.isArray(val) ? val : isValidValue(val) ? [val] : []
}

View File

@ -0,0 +1,61 @@
import type { TreeNodeData } from '@element-plus/components/tree/src/tree.type'
export function isValidValue(val: any) {
return val || val === 0
}
export function isValidArray(val: any) {
return Array.isArray(val) && val.length
}
export function toValidArray(val: any) {
return Array.isArray(val) ? val : isValidValue(val) ? [val] : []
}
type TreeCallback<T extends TreeNodeData, R> = (
data: T,
index: number,
array: T[],
parent?: T
) => R
type TreeFindCallback<T extends TreeNodeData> = TreeCallback<T, boolean>
export function treeFind<T extends TreeNodeData>(
treeData: T[],
findCallback: TreeFindCallback<T>,
getChildren: (data: T) => T[]
): T | undefined
export function treeFind<T extends TreeNodeData, R>(
treeData: T[],
findCallback: TreeFindCallback<T>,
getChildren: (data: T) => T[],
resultCallback?: TreeCallback<T, R>,
parent?: T
): R | undefined
export function treeFind<T extends TreeNodeData, R>(
treeData: T[],
findCallback: TreeFindCallback<T>,
getChildren: (data: T) => T[],
resultCallback?: TreeCallback<T, R>,
parent?: T
): T | R | undefined {
for (let i = 0; i < treeData.length; i++) {
const data = treeData[i]
if (findCallback(data, i, treeData, parent)) {
return resultCallback ? resultCallback(data, i, treeData, parent) : data
} else {
const children = getChildren(data)
if (isValidArray(children)) {
const find = treeFind(
children,
findCallback,
getChildren,
resultCallback,
data
)
if (find) return find
}
}
}
}