mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 11:17:46 +08:00
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:
parent
ede25ea5b0
commit
4fe9c2bd6c
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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: {
|
||||
|
@ -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] : []
|
||||
}
|
||||
|
61
packages/components/tree-select/src/utils.ts
Normal file
61
packages/components/tree-select/src/utils.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user