Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(components): [TreeSelect] checkbox interaction #8102

Merged
merged 9 commits into from
Jul 2, 2022
16 changes: 16 additions & 0 deletions docs/en-US/component/tree-select.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion docs/examples/tree-select/basic.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
26 changes: 25 additions & 1 deletion docs/examples/tree-select/check-strictly.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
27 changes: 25 additions & 2 deletions docs/examples/tree-select/multiple.vue
Original file line number Diff line number Diff line change
@@ -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 = [
{
Expand Down
55 changes: 55 additions & 0 deletions packages/components/tree-select/__tests__/tree-select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
19 changes: 18 additions & 1 deletion packages/components/tree-select/src/tree-select-option.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineComponent } from 'vue'
import { defineComponent, getCurrentInstance, nextTick } from 'vue'
import { ElOption } from '@element-plus/components/select'

const component = defineComponent({
Expand All @@ -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: {
Expand Down
108 changes: 70 additions & 38 deletions packages/components/tree-select/src/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,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'
Expand Down Expand Up @@ -66,6 +67,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,
Expand All @@ -76,17 +89,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 }) => {
Expand All @@ -113,14 +121,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
chenxch marked this conversation as resolved.
Show resolved Hide resolved

// 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)
Expand All @@ -134,28 +139,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] : []
}