From 790103596c0a2e701ea1089553c97236ea39fe98 Mon Sep 17 00:00:00 2001 From: jeremywu <591449570@qq.com> Date: Fri, 4 Dec 2020 13:39:01 +0800 Subject: [PATCH] fix(select): fix select value did not match issue (#808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 陈婉玉 --- packages/select/__tests__/select.spec.ts | 12 ++- packages/select/src/option.vue | 3 +- packages/select/src/select.vue | 3 + packages/select/src/token.ts | 7 +- packages/select/src/useOption.ts | 3 - packages/select/src/useSelect.ts | 104 +++++++++++++---------- 6 files changed, 73 insertions(+), 59 deletions(-) diff --git a/packages/select/__tests__/select.spec.ts b/packages/select/__tests__/select.spec.ts index a3e68979c3..fd741c4347 100644 --- a/packages/select/__tests__/select.spec.ts +++ b/packages/select/__tests__/select.spec.ts @@ -30,9 +30,9 @@ const _mount = (template: string, data: any = () => ({}), otherObj?): any => mou }) function getOptions(): HTMLElement[] { - return [...document.querySelectorAll( + return Array.from(document.querySelectorAll( '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(``, () => ({ 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() diff --git a/packages/select/src/option.vue b/packages/select/src/option.vue index 058719ceca..b481c8a701 100644 --- a/packages/select/src/option.vue +++ b/packages/select/src/option.vue @@ -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) { diff --git a/packages/select/src/select.vue b/packages/select/src/select.vue index 71cd1dbbda..a6102974ba 100644 --- a/packages/select/src/select.vue +++ b/packages/select/src/select.vue @@ -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, diff --git a/packages/select/src/token.ts b/packages/select/src/token.ts index 307fd701d4..6dadf660fb 100644 --- a/packages/select/src/token.ts +++ b/packages/select/src/token.ts @@ -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 = Symbol('SelectGroup') diff --git a/packages/select/src/useOption.ts b/packages/select/src/useOption.ts index ad55544d99..4fc324f91f 100644 --- a/packages/select/src/useOption.ts +++ b/packages/select/src/useOption.ts @@ -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 diff --git a/packages/select/src/useSelect.ts b/packages/select/src/useSelect.ts index 59dd8f374c..445c51ffdb 100644 --- a/packages/select/src/useSelect.ts +++ b/packages/select/src/useSelect.ts @@ -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,