mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
fix(select): fix select value did not match issue (#808)
* fix(select): fix select value did not match issue - Fix when select's options were added after select is mounted, the value will not match * fix: options.push cannot trigger watcher Co-authored-by: 陈婉玉 <simonaliachen@gmail.com>
This commit is contained in:
@@ -30,9 +30,9 @@ const _mount = (template: string, data: any = () => ({}), otherObj?): any => mou
|
||||
})
|
||||
|
||||
function getOptions(): HTMLElement[] {
|
||||
return [...document.querySelectorAll<HTMLElement>(
|
||||
return Array.from(document.querySelectorAll<HTMLElement>(
|
||||
'body > div:last-child .el-select-dropdown__item',
|
||||
)]
|
||||
))
|
||||
}
|
||||
|
||||
const getSelectVm = (configs: SelectProps = {}, options?) => {
|
||||
@@ -107,6 +107,10 @@ const getSelectVm = (configs: SelectProps = {}, options?) => {
|
||||
}
|
||||
|
||||
describe('Select', () => {
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
test('create', async () => {
|
||||
const wrapper = _mount(`<el-select v-model="value"></el-select>`, () => ({ value: '' }))
|
||||
expect(wrapper.classes()).toContain('el-select')
|
||||
@@ -296,10 +300,10 @@ describe('Select', () => {
|
||||
const select = wrapper.findComponent({ name: 'ElSelect' })
|
||||
const selectVm = select.vm as any
|
||||
const input = wrapper.find('input')
|
||||
await input.trigger('focus')
|
||||
input.element.focus()
|
||||
selectVm.selectedLabel = 'new'
|
||||
selectVm.debouncedOnInputChange()
|
||||
await selectVm.$nextTick()
|
||||
await nextTick()
|
||||
const options = [...getOptions()]
|
||||
const target = options.filter(option => option.textContent === 'new')
|
||||
target[0].click()
|
||||
|
||||
@@ -66,6 +66,7 @@ export default defineComponent({
|
||||
} = toRefs(states)
|
||||
|
||||
const vm = getCurrentInstance().proxy
|
||||
select.onOptionCreate(vm)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const { selected } = select
|
||||
@@ -79,8 +80,6 @@ export default defineComponent({
|
||||
}
|
||||
select.onOptionDestroy(select.options.map(item => item.value).indexOf(props.value))
|
||||
})
|
||||
select.options.push(vm)
|
||||
select.cachedOptions.push(vm)
|
||||
|
||||
function selectOptionClick() {
|
||||
if (props.disabled !== true && states.groupDisabled !== true) {
|
||||
|
||||
@@ -276,6 +276,7 @@ export default defineComponent({
|
||||
toggleLastOptionHitState,
|
||||
resetInputState,
|
||||
handleComposition,
|
||||
onOptionCreate,
|
||||
onOptionDestroy,
|
||||
handleMenuEnter,
|
||||
handleFocus,
|
||||
@@ -326,6 +327,7 @@ export default defineComponent({
|
||||
hoverIndex,
|
||||
handleOptionSelect,
|
||||
selectEmitter: states.selectEmitter,
|
||||
onOptionCreate,
|
||||
onOptionDestroy,
|
||||
selectWrapper,
|
||||
selected,
|
||||
@@ -368,6 +370,7 @@ export default defineComponent({
|
||||
if (!props.multiple && Array.isArray(props.modelValue)) {
|
||||
ctx.emit(UPDATE_MODEL_EVENT, '')
|
||||
}
|
||||
|
||||
return {
|
||||
selectSize,
|
||||
readonly,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { ComponentPublicInstance, InjectionKey } from 'vue'
|
||||
import type { Emitter } from 'mitt'
|
||||
|
||||
interface SelectGroupContext {
|
||||
@@ -23,8 +23,9 @@ export interface SelectContext {
|
||||
selected: any | any[]
|
||||
selectEmitter: Emitter
|
||||
setSelected(): void
|
||||
onOptionDestroy(i: number)
|
||||
handleOptionSelect(vm: unknown, byClick: boolean)
|
||||
onOptionCreate(vm: ComponentPublicInstance): void
|
||||
onOptionDestroy(i: number): void
|
||||
handleOptionSelect(vm: unknown, byClick: boolean): void
|
||||
}
|
||||
|
||||
export const selectGroupKey: InjectionKey<SelectGroupContext> = Symbol('SelectGroup')
|
||||
|
||||
@@ -53,9 +53,6 @@ export function useOption(props, states) {
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
select.optionsCount++
|
||||
select.filteredOptionsCount++
|
||||
|
||||
const contains = (arr = [], target) => {
|
||||
if (!isObject.value) {
|
||||
return arr && arr.indexOf(target) > -1
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'vue'
|
||||
import mitt from 'mitt'
|
||||
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
|
||||
import { EVENT_CODE } from '@element-plus/utils/aria'
|
||||
import { t } from '@element-plus/locale'
|
||||
import isServer from '@element-plus/utils/isServer'
|
||||
import scrollIntoView from '@element-plus/utils/scroll-into-view'
|
||||
@@ -21,8 +22,9 @@ import {
|
||||
} from '@element-plus/utils/util'
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/form'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import { toRawType } from '@vue/shared'
|
||||
import { isObject, toRawType } from '@vue/shared'
|
||||
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/form'
|
||||
|
||||
export function useSelectStates(props) {
|
||||
@@ -207,20 +209,25 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
ctx.emit('visible-change', val)
|
||||
})
|
||||
|
||||
watch(() => states.options, () => {
|
||||
if (isServer) return
|
||||
popper.value?.update()
|
||||
if (props.multiple) {
|
||||
resetInputHeight()
|
||||
}
|
||||
const inputs = selectWrapper.value.querySelectorAll('input')
|
||||
if ([].indexOf.call(inputs, document.activeElement) === -1) {
|
||||
setSelected()
|
||||
}
|
||||
if (props.defaultFirstOption && (props.filterable || props.remote) && states.filteredOptionsCount) {
|
||||
checkDefaultFirstOption()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
// fix `Array.prototype.push/splice/..` cannot trigger non-deep watcher
|
||||
// https://github.com/vuejs/vue-next/issues/2116
|
||||
() => ([...states.options]),
|
||||
() => {
|
||||
if (isServer) return
|
||||
popper.value?.update?.()
|
||||
if (props.multiple) {
|
||||
resetInputHeight()
|
||||
}
|
||||
const inputs = selectWrapper.value?.querySelectorAll('input') || []
|
||||
if ([].indexOf.call(inputs, document.activeElement) === -1) {
|
||||
setSelected()
|
||||
}
|
||||
if (props.defaultFirstOption && (props.filterable || props.remote) && states.filteredOptionsCount) {
|
||||
checkDefaultFirstOption()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(() => states.hoverIndex, val => {
|
||||
if (typeof val === 'number' && val > -1) {
|
||||
@@ -263,7 +270,7 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
}
|
||||
states.previousQuery = val
|
||||
nextTick(() => {
|
||||
if (states.visible) popper.value?.update()
|
||||
if (states.visible) popper.value?.update?.()
|
||||
})
|
||||
states.hoverIndex = -1
|
||||
if (props.multiple && props.filterable) {
|
||||
@@ -354,16 +361,16 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
|
||||
const getOption = value => {
|
||||
let option
|
||||
const isObject = toRawType(value).toLowerCase() === 'object'
|
||||
const isObjectValue = toRawType(value).toLowerCase() === 'object'
|
||||
const isNull = toRawType(value).toLowerCase() === 'null'
|
||||
const isUndefined = toRawType(value).toLowerCase() === 'undefined'
|
||||
|
||||
for (let i = states.cachedOptions.length - 1; i >= 0; i--) {
|
||||
const cachedOption = states.cachedOptions[i]
|
||||
const isEqual = isObject
|
||||
const isEqualValue = isObjectValue
|
||||
? getValueByPath(cachedOption.value, props.valueKey) === getValueByPath(value, props.valueKey)
|
||||
: cachedOption.value === value
|
||||
if (isEqual) {
|
||||
if (isEqualValue) {
|
||||
option = {
|
||||
value,
|
||||
currentLabel: cachedOption.currentLabel,
|
||||
@@ -372,9 +379,9 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
}
|
||||
}
|
||||
if (option) return option
|
||||
const label = (!isObject && !isNull && !isUndefined) ? value : ''
|
||||
const label = (!isObjectValue && !isNull && !isUndefined) ? value : ''
|
||||
const newOption = {
|
||||
value: value,
|
||||
value,
|
||||
currentLabel: label,
|
||||
}
|
||||
if (props.multiple) {
|
||||
@@ -399,7 +406,7 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
|
||||
const handleResize = () => {
|
||||
resetInputWidth()
|
||||
popper.value?.update()
|
||||
popper.value?.update?.()
|
||||
if (props.multiple) resetInputHeight()
|
||||
}
|
||||
|
||||
@@ -489,21 +496,18 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
}
|
||||
|
||||
const getValueIndex = (arr = [], value) => {
|
||||
const isObject = Object.prototype.toString.call(value).toLowerCase() === '[object object]'
|
||||
if (!isObject) {
|
||||
return arr.indexOf(value)
|
||||
} else {
|
||||
const valueKey = props.valueKey
|
||||
let index = -1
|
||||
arr.some((item, i) => {
|
||||
if (getValueByPath(item, valueKey) === getValueByPath(value, valueKey)) {
|
||||
index = i
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return index
|
||||
}
|
||||
if (!isObject(value)) return arr.indexOf(value)
|
||||
|
||||
const valueKey = props.valueKey
|
||||
let index = -1
|
||||
arr.some((item, i) => {
|
||||
if (getValueByPath(item, valueKey) === getValueByPath(value, valueKey)) {
|
||||
index = i
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return index
|
||||
}
|
||||
|
||||
const setSoftFocus = () => {
|
||||
@@ -526,7 +530,14 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
// scrollbar.value?.handleScroll()
|
||||
}
|
||||
|
||||
const onOptionDestroy = index => {
|
||||
const onOptionCreate = (vm: ComponentPublicInstance) => {
|
||||
states.optionsCount++
|
||||
states.filteredOptionsCount++
|
||||
states.options.push(vm)
|
||||
states.cachedOptions.push(vm)
|
||||
}
|
||||
|
||||
const onOptionDestroy = (index: number) => {
|
||||
if (index > -1) {
|
||||
states.optionsCount--
|
||||
states.filteredOptionsCount--
|
||||
@@ -534,8 +545,8 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resetInputState = e => {
|
||||
if (e.keyCode !== 8) toggleLastOptionHitState(false)
|
||||
const resetInputState = (e: KeyboardEvent) => {
|
||||
if (e.code !== EVENT_CODE.backspace) toggleLastOptionHitState(false)
|
||||
states.inputLength = input.value.length * 15 + 20
|
||||
resetInputHeight()
|
||||
}
|
||||
@@ -588,7 +599,7 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
reference.value.blur()
|
||||
}
|
||||
|
||||
const handleBlur = event => {
|
||||
const handleBlur = (event: Event) => {
|
||||
// https://github.com/ElemeFE/element/pull/10822
|
||||
nextTick(() => {
|
||||
if (states.isSilentBlur) {
|
||||
@@ -600,7 +611,7 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
states.softFocus = false
|
||||
}
|
||||
|
||||
const handleClearClick = event => {
|
||||
const handleClearClick = (event: Event) => {
|
||||
deleteSelected(event)
|
||||
}
|
||||
|
||||
@@ -633,11 +644,9 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
}
|
||||
|
||||
const getValueKey = item => {
|
||||
if (Object.prototype.toString.call(item.value).toLowerCase() !== '[object object]') {
|
||||
return item.value
|
||||
} else {
|
||||
return getValueByPath(item.value, props.valueKey)
|
||||
}
|
||||
return isObject(item.value)
|
||||
? getValueByPath(item.value, props.valueKey)
|
||||
: item.value
|
||||
}
|
||||
|
||||
const optionsAllDisabled = computed(() => states.options.filter(option => option.visible).every(option => option.disabled))
|
||||
@@ -694,6 +703,7 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
toggleLastOptionHitState,
|
||||
resetInputState,
|
||||
handleComposition,
|
||||
onOptionCreate,
|
||||
onOptionDestroy,
|
||||
handleMenuEnter,
|
||||
handleFocus,
|
||||
|
||||
Reference in New Issue
Block a user