feat(components): add tree select component (#6843)

* feat(ElTreeSelect): add tree select base component

* refactor(ElTreeSelect): use render function and move select/tree props to them self module

* fix(ElTreeSelect): init value not checked

* fix(ElTreeSelect): `toArray` ignores valid values

* fix(ElTreeSelect): expose not working when defined on mounted

* fix(ElTreeSelect): watch `modelValue` deep

* test(ElTreeSelect): add base unit test

* perf(ElTreeSelect): default slot should be a function

* fix(ElTreeSelect): `onNodeClick` can not call,

* test(ElTreeSelect): update unit test

* fix(ElTreeSelect): `onNodeClick` can not call,

* fix(ElTreeSelect): remove folder node when `checkStrictly` is false

* feat(ElTreeSelect): export `ElTreeSelect`

* fix(ElTreeSelect): `filterMethod` conflicts with `filterNodeMethod`

* docs(ElTreeSelect): add component docs

* fix(ElTreeSelect): fix lint

* docs(ElTreeSelect): fix lazy loading requires non-leaf nodes, and change mock labels

* docs(ElTreeSelect): the link address of the attributes is incorrect

* docs(ElTreeSelect): `dropdown` doesn't need the `-` symbol

* refactor(ElTreeSelect): use alias path and make sure vue is above to components

* refactor(ElTreeSelect): use a unified namespace for styles

* docs(ElTreeSelect): change option labels in default slots

* refactor(ElTreeSelect): import `ElOption` using unified entry and change the way to override the select click event

* style(ElTreeSelect): sort imports

* docs(ElTreeSelect): update the documentation for special codes

* refactor(ElTreeSelect): keep it consistent with the select style

* refactor(ElTreeSelect): use `isFunction` from `@element-plus/utils`

* refactor(ElTreeSelect): use single closing tag when no subset

* docs(ElTreeSelect): set `TreeSelect` promotion as `2.1.8`
This commit is contained in:
虞金攀 2022-04-02 15:15:33 +08:00 committed by GitHub
parent ada12878d1
commit 904aa0e21b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1334 additions and 0 deletions

View File

@ -206,6 +206,11 @@
"link": "/tree",
"text": "Tree"
},
{
"link": "/tree-select",
"text": "TreeSelect",
"promotion": "2.1.8"
},
{
"link": "/tree-v2",
"text": "Virtualized Tree"

View File

@ -0,0 +1,93 @@
---
title: TreeSelect
lang: en-US
---
# TreeSelect
The tree selector of the dropdown menu,
it combines the functions of components `el-tree` and `el-select`.
## Basic usage
Selector for tree structures.
:::demo
tree-select/basic
:::
## Select any level
When using the `check-strictly=true` attribute, any node can be checked,
otherwise only leaf nodes are supported.
:::demo
tree-select/check-strictly
:::
## Multiple Selection
Multiple selection using clicks or checkbox.
:::demo
tree-select/multiple
:::
## Disabled Selection
Disable options using the disabled field.
:::demo
tree-select/disabled
:::
## Filterable
Use keyword filtering or custom filtering methods.
`filterMethod` can custom filter method for data,
`filterNodeMethod` can custom filter method for data node.
:::demo
tree-select/filterable
:::
## Custom content
Contents of custom tree nodes.
:::demo
tree-select/slots
:::
## LazyLoad
Lazy loading of tree nodes, suitable for large data lists.
:::demo
tree-select/lazy
:::
## Attributes
Since this component combines the functions of components `el-tree` and `el-select`,
the original properties have not been changed, so no repetition here,
and please go to the original component to view the documentation.
| Attributes | Methods | Events | Slots |
| --------------------------------------- | ----------------------------- | ----------------------------------- | ---------------------------------- |
| [tree](./tree.md#attributes) | [tree](./tree.md#method) | [tree](./tree.md#events) | [tree](./tree.md#slots) |
| [select](./select.md#select-attributes) | [select](./select.md#methods) | [select](./select.md#select-events) | [select](./select.md#select-slots) |

View File

@ -0,0 +1,81 @@
<template>
<el-tree-select v-model="value" :data="data" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const data = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
</script>
```

View File

@ -0,0 +1,81 @@
<template>
<el-tree-select v-model="value" :data="data" check-strictly />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const data = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
</script>
```

View File

@ -0,0 +1,84 @@
<template>
<el-tree-select v-model="value" :data="data" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const data = [
{
value: '1',
label: 'Level one 1',
disabled: true,
children: [
{
value: '1-1',
label: 'Level two 1-1',
disabled: true,
children: [
{
disabled: true,
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
</script>
```

View File

@ -0,0 +1,104 @@
<template>
<el-tree-select v-model="value" :data="data" filterable />
<el-divider />
filter method:
<el-tree-select
v-model="value"
:data="data"
:filter-method="filterMethod"
filterable
/>
<el-divider />
filter node method:
<el-tree-select
v-model="value"
:data="data"
:filter-node-method="filterNodeMethod"
filterable
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const sourceData = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
const data = ref(sourceData)
const filterMethod = (value) => {
data.value = [...sourceData].filter((item) => item.label.includes(value))
}
const filterNodeMethod = (value, data) => data.label.includes(value)
</script>
```

View File

@ -0,0 +1,36 @@
<template>
<el-tree-select v-model="value" lazy :load="load" :props="props" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const props = {
label: 'label',
children: 'children',
isLeaf: 'isLeaf',
}
let id = 0
const load = (node, resolve) => {
if (node.isLeaf) return resolve([])
setTimeout(() => {
resolve([
{
value: ++id,
label: `lazy load node${id}`,
},
{
value: ++id,
label: `lazy load node${id}`,
isLeaf: true,
},
])
}, 400)
}
</script>
```

View File

@ -0,0 +1,84 @@
<template>
<el-tree-select v-model="value" :data="data" multiple />
<el-divider />
show checkbox:
<el-tree-select v-model="value" :data="data" multiple show-checkbox />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const data = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
</script>
```

View File

@ -0,0 +1,104 @@
<template>
<el-tree-select v-model="value" :data="data">
<template #default="{ data: { label } }">
{{ label }}<span style="color: gray">(suffix)</span></template
>
</el-tree-select>
<el-divider />
use render content:
<el-tree-select
v-model="value"
:data="data"
:render-content="renderContent"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const renderContent = (h, { data }) => {
return h(
'span',
{
style: {
color: 'orange',
},
},
data.label
)
}
const data = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
</script>
```

View File

@ -61,6 +61,7 @@ export * from './timeline'
export * from './tooltip'
export * from './transfer'
export * from './tree'
export * from './tree-select'
export * from './tree-v2'
export * from './upload'
export * from './virtual-list'

View File

@ -0,0 +1,312 @@
import { h, nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils'
import TreeSelect from '../src/tree-select.vue'
import type { RenderFunction } from 'vue'
import type { VueWrapper } from '@vue/test-utils'
import type ElSelect from '@element-plus/components/select'
import type ElTree from '@element-plus/components/tree'
const createComponent = ({
slots = {},
props = {},
}: {
slots?: Record<string, any>
props?: typeof TreeSelect['props']
} = {}) => {
// vm can not get component expose, use ref
const wrapperRef = ref()
const value = props.modelValue || ref('')
const wrapper = mount({
data() {
return {
modelValue: value,
data: [
{
value: 1,
label: '一级 1',
children: [
{
value: 11,
label: '二级 1-1',
children: [
{
value: 111,
label: '三级 1-1',
},
],
},
],
},
],
'onUpdate:modelValue': (val: string) => (value.value = val),
...props,
}
},
render() {
return h(
TreeSelect,
{
...this.$data,
ref: (val: object) => (wrapperRef.value = val),
},
slots
)
},
})
return {
wrapper,
getWrapperRef: () =>
new Promise((resolve) =>
nextTick(() => resolve(wrapperRef.value))
) as Promise<InstanceType<typeof ElTree> & InstanceType<typeof ElSelect>>,
select: wrapper.findComponent({ name: 'ElSelect' }) as VueWrapper<
InstanceType<typeof ElSelect>
>,
tree: wrapper.findComponent({ name: 'ElTree' }) as VueWrapper<
InstanceType<typeof ElTree>
>,
}
}
describe('TreeSelect.vue', () => {
test('render test', async () => {
const { wrapper, tree } = createComponent({
props: {
defaultExpandAll: true,
},
})
expect(wrapper.find('.el-tree')).toBeTruthy()
expect(wrapper.find('.el-select')).toBeTruthy()
expect(tree.findAll('.el-tree > .el-tree-node').length).toBe(1)
expect(tree.findAll('.el-tree .el-tree-node').length).toBe(3)
expect(tree.findAll('.el-tree .el-select-dropdown__item').length).toBe(3)
wrapper.vm.data[0].children = []
await nextTick()
expect(tree.findAll('.el-tree .el-tree-node').length).toBe(1)
})
test('modelValue', async () => {
const value = ref(1)
const { getWrapperRef, select, tree } = createComponent({
props: {
modelValue: value,
checkStrictly: true,
showCheckbox: true,
},
})
const wrapperRef = await getWrapperRef()
await nextTick()
expect(select.vm.modelValue).toBe(1)
expect(wrapperRef.getCheckedKeys()).toEqual([1])
value.value = 11
await nextTick(nextTick)
expect(select.vm.modelValue).toBe(11)
expect(wrapperRef.getCheckedKeys()).toEqual([11])
await tree
.findAll('.el-select-dropdown__item')
.slice(-1)[0]
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(111)
expect(wrapperRef.getCheckedKeys()).toEqual([111])
await tree.find('.el-tree-node__content').trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(1)
expect(wrapperRef.getCheckedKeys()).toEqual([1])
await tree.findAll('.el-checkbox')[1].trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(11)
expect(wrapperRef.getCheckedKeys()).toEqual([11])
})
test('disabled', async () => {
const { wrapper, tree } = createComponent({
props: {
data: [
{
value: '1',
label: '1',
children: [
{
value: '2',
label: '2',
disabled: true,
},
],
},
],
showCheckbox: true,
checkStrictly: true,
defaultExpandAll: true,
},
})
await nextTick()
await tree.find('.el-tree-node').trigger('click')
await tree.find('.el-tree-node .el-checkbox.is-disabled').trigger('click')
await tree
.find('.el-tree-node .el-select-dropdown__item.is-disabled')
.trigger('click')
await nextTick()
expect(wrapper.vm.modelValue).toBe('1')
})
test('multiple', async () => {
const value = ref([1])
const { getWrapperRef, select, tree } = createComponent({
props: {
modelValue: value,
checkStrictly: true,
showCheckbox: true,
multiple: true,
},
})
const wrapperRef = await getWrapperRef()
await nextTick()
expect(select.vm.modelValue).toEqual([1])
expect(wrapperRef.getCheckedKeys()).toEqual([1])
value.value = [11]
await nextTick(nextTick)
expect(select.vm.modelValue).toEqual([11])
expect(wrapperRef.getCheckedKeys()).toEqual([11])
await tree
.findAll('.el-select-dropdown__item')
.slice(-1)[0]
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([11, 111])
expect(wrapperRef.getCheckedKeys()).toEqual([11, 111])
await tree.find('.el-tree-node__content').trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([11, 111, 1])
expect(wrapperRef.getCheckedKeys()).toEqual([1, 11, 111])
await tree.findAll('.el-checkbox')[1].trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([1, 111])
expect(wrapperRef.getCheckedKeys()).toEqual([1, 111])
})
test('filter', async () => {
const { select, tree } = createComponent({
props: {
filterable: true,
},
})
select.vm.query = '一级 1'
await nextTick()
expect(tree.findAll('.el-tree-node').length).toBe(1)
})
test('props', async () => {
const { wrapper, select, tree } = createComponent({
props: {
data: [
{
id: '1',
name: '1',
childrens: [
{
id: '2',
name: '2',
},
],
},
],
props: {
label: 'name',
children: 'childrens',
},
valueKey: 'id',
},
})
await nextTick()
expect(tree.find('.el-tree-node').text()).toBe('1')
wrapper.vm.modelValue = '2'
await nextTick()
expect(select.vm.selectedLabel).toBe('2')
})
test('slots', async () => {
const { select, tree } = createComponent({
slots: {
default: ({ data }: { data: { label: string } }) => `123${data.label}`,
prefix: () => 'prefix',
},
})
await nextTick()
expect(tree.find('.el-select-dropdown__item').text()).toBe('123一级 1')
expect(select.find('.el-input__prefix-inner').text()).toBe('prefix')
})
test('renderContent', async () => {
const { tree } = createComponent({
props: {
renderContent: (
h: RenderFunction,
{ data }: { data: { label: string } }
) => {
return `123${data.label}`
},
},
})
await nextTick()
expect(tree.find('.el-select-dropdown__item').text()).toBe('123一级 1')
})
test('lazy', async () => {
const { tree } = createComponent({
props: {
data: [
{
value: 1,
label: 1,
},
],
lazy: true,
load: (node: object, resolve: (p: any) => any[]) => {
resolve([{ value: 2, label: 2, isLeaf: true }])
},
},
})
await nextTick()
await tree.find('.el-tree-node').trigger('click')
await nextTick()
expect(tree.find('.el-tree-node .el-tree-node').text()).toBe('2')
})
test('events', async () => {
const onNodeClick = jest.fn()
const { tree } = createComponent({
props: {
onNodeClick,
},
})
await nextTick()
await tree.find('.el-tree-node').trigger('click')
await nextTick()
expect(onNodeClick).toBeCalled()
})
})

View File

@ -0,0 +1,13 @@
import TreeSelect from './src/tree-select.vue'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils'
TreeSelect.install = (app: App): void => {
app.component(TreeSelect.name, TreeSelect)
}
const _TreeSelect = TreeSelect as SFCWithInstall<typeof TreeSelect>
export default _TreeSelect
export const ElTreeSelect = _TreeSelect

View File

@ -0,0 +1,50 @@
import { computed, nextTick, toRefs } from 'vue'
import { pick } from 'lodash-unified'
import ElSelect from '@element-plus/components/select'
import { useNamespace } from '@element-plus/hooks'
import type { Ref } from 'vue'
import type ElTree from '@element-plus/components/tree'
export const useSelect = (
props,
{ attrs },
{
tree,
key,
}: {
select: Ref<InstanceType<typeof ElSelect> | undefined>
tree: Ref<InstanceType<typeof ElTree> | undefined>
key: Ref<string>
}
) => {
const ns = useNamespace('tree-select')
const result = {
...pick(toRefs(props), Object.keys(ElSelect.props)),
...attrs,
valueKey: key,
popperClass: computed(() => {
const classes = [ns.e('popper')]
if (props.popperClass) classes.push(props.popperClass)
return classes.join(' ')
}),
filterMethod: (keyword = '') => {
if (props.filterMethod) props.filterMethod(keyword)
nextTick(() => {
// let tree node expand only, same with tree filter
tree.value?.filter(keyword)
})
},
// clear filter text when visible change
onVisibleChange: (visible: boolean) => {
attrs.onVisibleChange?.(visible)
if (props.filterable && visible) {
result.filterMethod()
}
},
}
return result
}

View File

@ -0,0 +1,22 @@
import { defineComponent } from 'vue'
import { ElOption } from '@element-plus/components/select'
const component = defineComponent({
extends: ElOption,
setup(props, ctx) {
const result = (ElOption.setup as NonNullable<any>)(props, ctx)
// use methods.selectOptionClick
delete result.selectOptionClick
return result
},
methods: {
selectOptionClick() {
// $el.parentElement => el-tree-node__content
this.$el.parentElement.click()
},
},
})
export default component

View File

@ -0,0 +1,83 @@
<script lang="ts">
import { computed, defineComponent, h, onMounted, reactive, ref } from 'vue'
import { pick } from 'lodash-unified'
import ElSelect from '@element-plus/components/select'
import ElTree from '@element-plus/components/tree'
import { useSelect } from './select'
import { useTree } from './tree'
export default defineComponent({
name: 'ElTreeSelect',
props: {
...ElSelect.props,
...ElTree.props,
},
setup(props, context) {
const { slots, expose } = context
const select = ref<InstanceType<typeof ElSelect>>()
const tree = ref<InstanceType<typeof ElTree>>()
const key = computed(() => props.valueKey || props.nodeKey || 'value')
const selectProps = useSelect(props, context, { select, tree, key })
const treeProps = useTree(props, context, { select, tree, key })
// expose ElTree/ElSelect methods
const methods = reactive({})
expose(methods)
onMounted(() => {
Object.assign(methods, {
...pick(tree.value, [
'filter',
'updateKeyChildren',
'getCheckedNodes',
'setCheckedNodes',
'getCheckedKeys',
'setCheckedKeys',
'setChecked',
'getHalfCheckedNodes',
'getHalfCheckedKeys',
'getCurrentKey',
'getCurrentNode',
'setCurrentKey',
'setCurrentNode',
'getNode',
'remove',
'append',
'insertBefore',
'insertAfter',
]),
...pick(select.value, ['focus', 'blur']),
})
})
return () =>
h(
ElSelect,
/**
* 1. The `props` is processed into `Refs`, but `v-bind` and
* render function props cannot read `Refs`, so use `reactive`
* unwrap the `Refs` and keep reactive.
* 2. The keyword `ref` requires `Ref`, but `reactive` broke it,
* so use function.
*/
reactive({
...selectProps,
ref: (ref) => (select.value = ref),
}),
{
...slots,
default: () =>
h(
ElTree,
reactive({
...treeProps,
ref: (ref) => (tree.value = ref),
})
),
}
)
},
})
</script>

View File

@ -0,0 +1,136 @@
import { computed, nextTick, toRefs, watch } from 'vue'
import { isEqual, pick } from 'lodash-unified'
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 type { Ref } from 'vue'
import type ElSelect from '@element-plus/components/select'
import type Node from '@element-plus/components/tree/src/model/node'
import type { TreeNodeData } from '@element-plus/components/tree/src/tree.type'
export const useTree = (
props,
{ attrs, slots, emit },
{
select,
tree,
key,
}: {
select: Ref<InstanceType<typeof ElSelect> | undefined>
tree: Ref<InstanceType<typeof ElTree> | undefined>
key: Ref<string>
}
) => {
watch(
() => props.modelValue,
() => {
if (props.showCheckbox) {
nextTick(() => {
const treeInstance = tree.value
if (
treeInstance &&
!isEqual(
treeInstance.getCheckedKeys(),
toValidArray(props.modelValue)
)
) {
treeInstance.setCheckedKeys(toValidArray(props.modelValue))
}
})
}
},
{
immediate: true,
deep: true,
}
)
const propsMap = computed(() => ({
value: key.value,
...props.props,
}))
const getNodeValByProp = (
prop: 'value' | 'label' | 'children' | 'disabled' | 'isLeaf',
data: TreeNodeData
) => {
const propVal = propsMap.value[prop]
if (isFunction(propVal)) {
return propVal(
data,
tree.value?.getNode(getNodeValByProp('value', data)) as Node
)
} else {
return data[propVal as string]
}
}
return {
...pick(toRefs(props), Object.keys(ElTree.props)),
...attrs,
nodeKey: key,
defaultExpandedKeys: computed(() =>
props.defaultExpandedKeys
? props.defaultExpandedKeys.concat(props.modelValue)
: toValidArray(props.modelValue)
),
renderContent: (h, { node, data, store }) => {
return h(
TreeSelectOption,
{
value: getNodeValByProp('value', data),
label: getNodeValByProp('label', data),
disabled: getNodeValByProp('disabled', data),
},
props.renderContent
? () => props.renderContent(h, { node, data, store })
: slots.default
? () => slots.default({ node, data, store })
: undefined
)
},
filterNodeMethod: (value, data, node) => {
if (props.filterNodeMethod)
return props.filterNodeMethod(value, data, node)
if (!value) return true
return getNodeValByProp('label', data)?.includes(value)
},
onNodeClick: (data, node, e) => {
attrs.onNodeClick?.(data, node, e)
if (props.checkStrictly || node.isLeaf) {
if (!getNodeValByProp('disabled', data)) {
const option = select.value?.options.get(
getNodeValByProp('value', data)
)
select.value?.handleOptionSelect(option, true)
}
} else {
e.ctx.handleExpandIconClick()
}
},
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 value = getNodeValByProp('value', data)
emit(
UPDATE_MODEL_EVENT,
props.multiple
? checkedKeys
: checkedKeys.includes(value)
? value
: undefined
)
},
}
}
function toValidArray(val: any) {
return Array.isArray(val) ? val : val || val === 0 ? [val] : []
}

View File

@ -0,0 +1,3 @@
import '@element-plus/components/select/style/css'
import '@element-plus/components/tree/style/css'
import '@element-plus/theme-chalk/src/tree-select/css'

View File

@ -0,0 +1,3 @@
import '@element-plus/components/select/style'
import '@element-plus/components/tree/style'
import '@element-plus/theme-chalk/src/tree-select.scss'

View File

@ -94,6 +94,7 @@ import { ElTooltip } from '@element-plus/components/tooltip'
import { ElTooltipV2 } from '@element-plus/components/tooltip-v2'
import { ElTransfer } from '@element-plus/components/transfer'
import { ElTree } from '@element-plus/components/tree'
import { ElTreeSelect } from '@element-plus/components/tree-select'
import { ElTreeV2 } from '@element-plus/components/tree-v2'
import { ElUpload } from '@element-plus/components/upload'
import type { Plugin } from 'vue'
@ -188,6 +189,7 @@ export default [
ElTooltipV2,
ElTransfer,
ElTree,
ElTreeSelect,
ElTreeV2,
ElUpload,
] as Plugin[]

View File

@ -92,6 +92,7 @@
@use './tooltip-v2.scss';
@use './transfer.scss';
@use './tree.scss';
@use './tree-select.scss';
@use './upload.scss';
@use './virtual-list.scss';
@use './popper.scss';

View File

@ -0,0 +1,36 @@
@use 'mixins/mixins' as *;
@use 'mixins/var' as *;
@use 'common/var' as *;
@include b(tree-select) {
@include set-component-css-var('tree', $tree);
}
@include b(tree-select) {
@include e(popper) {
// padding-left same with select option
.#{$namespace}-tree-node__expand-icon {
margin-left: 8px;
}
// remove icon when show checkbox
.#{$namespace}-tree-node.is-checked
> .#{$namespace}-tree-node__content
.#{$namespace}-select-dropdown__item.selected::after {
content: none;
}
.#{$namespace}-select-dropdown__item {
flex: 1;
background: transparent !important;
// padding-left move to `el-tree-node__expand-icon`
padding-left: 0;
// fix: select height > tree node height
// https://github.com/yujinpan/el-select-tree/pull/33
height: 20px;
line-height: 20px;
}
}
}