fix(components): [select] select value label rendering error (#20769)

* fix(components): [select] select value label rendering error

* chore: comment

* fix: update

* fix: update

* feat: manual render

* refactor: immediate watch

* test: update

* test: with multiple
This commit is contained in:
btea
2025-05-21 13:54:59 +08:00
committed by GitHub
parent 73150c1214
commit 8ddbb1d85a
4 changed files with 176 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import { defineComponent, markRaw, nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, test, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'
import { EVENT_CODE } from '@element-plus/constants'
import { ArrowDown, CaretTop, CircleClose } from '@element-plus/icons-vue'
import { usePopperContainerId } from '@element-plus/hooks'
@@ -307,11 +307,17 @@ const WRAPPER_CLASS_NAME = 'el-select__wrapper'
const OPTION_ITEM_CLASS_NAME = 'el-select-dropdown__item'
const PLACEHOLDER_CLASS_NAME = 'el-select__placeholder'
const DEFAULT_PLACEHOLDER = 'Select'
const TAG_NAME = `${WRAPPER_CLASS_NAME} .el-tag`
describe('Select', () => {
let wrapper: ReturnType<typeof _mount>
beforeEach(() => {
// This is convenient for testing the default value label rendering when persistent is false.
process.env.RUN_TEST_FILE_NAME = 'select'
})
afterEach(() => {
document.body.innerHTML = ''
delete process.env.RUN_TEST_FILE_NAME
})
test('create', async () => {
@@ -380,6 +386,140 @@ describe('Select', () => {
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe('双皮奶')
})
test('the scenario of rendering label when there is a default value and persistent is false', async () => {
wrapper = _mount(
`
<el-select v-model="value" :persistent="false">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>
`,
() => ({
options: [
{
value: '选项1',
label: '黄金糕',
},
{
value: '选项2',
label: '双皮奶',
},
],
value: '选项2',
})
)
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe('双皮奶')
})
test('when there is a default value and persistent is false, render the label and dynamically modify options', async () => {
wrapper = _mount(
`
<el-select v-model="value" :persistent="false">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>
`,
() => ({
options: [],
value: '选项2',
})
)
await nextTick()
const vm = wrapper.vm as any
vm.options = [
{
value: '选项1',
label: '黄金糕',
},
{
value: '选项2',
label: '双皮奶',
},
]
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe('双皮奶')
})
test('multiple is true and persistent is false', async () => {
wrapper = _mount(
`
<el-select v-model="value" :persistent="false" multiple>
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>
`,
() => ({
options: [
{
value: '选项1',
label: '黄金糕',
},
{
value: '选项2',
label: '双皮奶',
},
],
value: ['选项2'],
})
)
await nextTick()
const tags = wrapper.findAll(`.${TAG_NAME}`)
expect(tags.length).toBe(1)
expect(tags[0].text()).toBe('双皮奶')
})
test('multiple is true and persistent is false, render the label and dynamically modify options', async () => {
wrapper = _mount(
`
<el-select v-model="value" :persistent="false" multiple>
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>
`,
() => ({
options: [],
value: ['选项2'],
})
)
await nextTick()
const vm = wrapper.vm as any
vm.options = [
{
value: '选项1',
label: '黄金糕',
},
{
value: '选项2',
label: '双皮奶',
},
]
await nextTick()
const tags = wrapper.findAll(`.${TAG_NAME}`)
expect(tags.length).toBe(1)
expect(tags[0].text()).toBe('双皮奶')
})
test('expose select label', async () => {
wrapper = _mount(
`
@@ -1291,8 +1431,8 @@ describe('Select', () => {
const triggerWrappers = wrapper.findAll('.el-tooltip__trigger')
expect(triggerWrappers[0]).toBeDefined()
const tags = document.querySelectorAll('.el-select__tags-text')
expect(tags.length).toBe(4)
expect(tags[3].textContent).toBe('蚵仔煎')
expect(tags.length).toBe(2)
expect(tags[1].textContent).toBe(' + 2')
})
test('multiple select with maxCollapseTags', async () => {

View File

@@ -300,14 +300,14 @@
</template>
<script lang="ts">
import { computed, defineComponent, provide, reactive, toRefs } from 'vue'
import { computed, defineComponent, provide, reactive, toRefs, watch } from 'vue'
import { ClickOutside } from '@element-plus/directives'
import ElTooltip from '@element-plus/components/tooltip'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTag from '@element-plus/components/tag'
import ElIcon from '@element-plus/components/icon'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { isArray } from '@element-plus/utils'
import { flattedChildren, isArray, isObject } from '@element-plus/utils'
import { useCalcInputWidth } from '@element-plus/hooks'
import ElOption from './option.vue'
import ElSelectMenu from './select-dropdown.vue'
@@ -316,6 +316,7 @@ import { selectKey } from './token'
import ElOptions from './options'
import { SelectProps } from './select'
import type { VNode } from 'vue';
import type { SelectContext } from './type'
const COMPONENT_NAME = 'ElSelect'
@@ -344,7 +345,7 @@ export default defineComponent({
'popup-scroll',
],
setup(props, { emit }) {
setup(props, { emit, slots }) {
const modelValue = computed(() => {
const { modelValue: rawModelValue, multiple } = props
const fallback = multiple ? [] : undefined
@@ -365,6 +366,31 @@ export default defineComponent({
const API = useSelect(_props, emit)
const { calculatorRef, inputStyle } = useCalcInputWidth()
const manuallyRenderSlots = (defaultSlots: VNode[] | undefined) => {
// After option rendering is completed, the useSelect internal state can collect the value of each option.
// If the persistent value is false, option will not be rendered by default, so in this case,
// manually render and load option data here.
if (!props.persistent && defaultSlots) {
const children = flattedChildren(defaultSlots) as VNode[]
children.filter((item) => {
// @ts-expect-error
return isObject(item) && item!.type.name === 'ElOption'
}).forEach(item => {
const obj = { ...item.props } as any
obj.currentLabel = obj.label || (isObject(obj.value) ? '' : obj.value)
API.onOptionCreate(obj)
})
}
}
watch(() => {
const currentSlot = slots.default?.()
return currentSlot
}, (newSlot) => {
manuallyRenderSlots(newSlot)
}, {
immediate: true,
})
provide(
selectKey,
reactive({

View File

@@ -87,7 +87,9 @@ const persistentRef = computed(() => {
// For testing, we would always want the content to be rendered
// to the DOM, so we need to return true here.
if (process.env.NODE_ENV === 'test') {
return true
if (process.env.RUN_TEST_FILE_NAME !== 'select') {
return true
}
}
return props.persistent
})

1
typings/env.d.ts vendored
View File

@@ -5,6 +5,7 @@ declare global {
const process: {
env: {
NODE_ENV: string
RUN_TEST_FILE_NAME: string
}
}