mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-17 11:49:41 +08:00
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:
parent
ada12878d1
commit
904aa0e21b
@ -206,6 +206,11 @@
|
||||
"link": "/tree",
|
||||
"text": "Tree"
|
||||
},
|
||||
{
|
||||
"link": "/tree-select",
|
||||
"text": "TreeSelect",
|
||||
"promotion": "2.1.8"
|
||||
},
|
||||
{
|
||||
"link": "/tree-v2",
|
||||
"text": "Virtualized Tree"
|
||||
|
93
docs/en-US/component/tree-select.md
Normal file
93
docs/en-US/component/tree-select.md
Normal 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) |
|
81
docs/examples/tree-select/basic.vue
Normal file
81
docs/examples/tree-select/basic.vue
Normal 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>
|
||||
```
|
81
docs/examples/tree-select/check-strictly.vue
Normal file
81
docs/examples/tree-select/check-strictly.vue
Normal 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>
|
||||
```
|
84
docs/examples/tree-select/disabled.vue
Normal file
84
docs/examples/tree-select/disabled.vue
Normal 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>
|
||||
```
|
104
docs/examples/tree-select/filterable.vue
Normal file
104
docs/examples/tree-select/filterable.vue
Normal 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>
|
||||
```
|
36
docs/examples/tree-select/lazy.vue
Normal file
36
docs/examples/tree-select/lazy.vue
Normal 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>
|
||||
```
|
84
docs/examples/tree-select/multiple.vue
Normal file
84
docs/examples/tree-select/multiple.vue
Normal 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>
|
||||
```
|
104
docs/examples/tree-select/slots.vue
Normal file
104
docs/examples/tree-select/slots.vue
Normal 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>
|
||||
```
|
@ -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'
|
||||
|
312
packages/components/tree-select/__tests__/tree-select.spec.ts
Normal file
312
packages/components/tree-select/__tests__/tree-select.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
13
packages/components/tree-select/index.ts
Normal file
13
packages/components/tree-select/index.ts
Normal 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
|
50
packages/components/tree-select/src/select.ts
Normal file
50
packages/components/tree-select/src/select.ts
Normal 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
|
||||
}
|
22
packages/components/tree-select/src/tree-select-option.ts
Normal file
22
packages/components/tree-select/src/tree-select-option.ts
Normal 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
|
83
packages/components/tree-select/src/tree-select.vue
Normal file
83
packages/components/tree-select/src/tree-select.vue
Normal 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>
|
136
packages/components/tree-select/src/tree.ts
Normal file
136
packages/components/tree-select/src/tree.ts
Normal 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] : []
|
||||
}
|
3
packages/components/tree-select/style/css.ts
Normal file
3
packages/components/tree-select/style/css.ts
Normal 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'
|
3
packages/components/tree-select/style/index.ts
Normal file
3
packages/components/tree-select/style/index.ts
Normal 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'
|
@ -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[]
|
||||
|
@ -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';
|
||||
|
36
packages/theme-chalk/src/tree-select.scss
Normal file
36
packages/theme-chalk/src/tree-select.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user