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:
jeremywu
2020-12-04 13:39:01 +08:00
committed by GitHub
parent a293a31584
commit 790103596c
6 changed files with 73 additions and 59 deletions

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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')

View File

@@ -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

View File

@@ -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,