diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts index 05ee59d9010..6cc7b53e2c2 100644 --- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts @@ -1037,15 +1037,25 @@ describe('vModel', () => { await nextTick() expect(data.value).toMatchObject([fooValue, barValue]) + // reset foo.selected = false bar.selected = false + triggerEvent('change', input) + await nextTick() + expect(data.value).toMatchObject([]) + data.value = [fooValue, barValue] await nextTick() expect(foo.selected).toEqual(true) expect(bar.selected).toEqual(true) + // reset foo.selected = false bar.selected = false + triggerEvent('change', input) + await nextTick() + expect(data.value).toMatchObject([]) + data.value = [{ foo: 1 }, { bar: 1 }] await nextTick() // looseEqual diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 7b387096b68..c581cb10589 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -3,6 +3,7 @@ import { type DirectiveHook, type ObjectDirective, type VNode, + nextTick, warn, } from '@vue/runtime-core' import { addEventListener } from '../modules/events' @@ -38,7 +39,9 @@ function onCompositionEnd(e: Event) { const assignKey = Symbol('_assign') -type ModelDirective = ObjectDirective +type ModelDirective = ObjectDirective< + T & { [assignKey]: AssignerFn; _assigning?: boolean } +> // We are exporting the v-model runtime directly as vnode hooks so that it can // be tree-shaken in case v-model is never used. @@ -197,25 +200,37 @@ export const vModelSelect: ModelDirective = { : selectedVal : selectedVal[0], ) + el._assigning = true + nextTick(() => { + el._assigning = false + }) }) el[assignKey] = getModelAssigner(vnode) }, // set value in mounted & updated because expects an Array or Set value for its binding, ` + @@ -223,12 +238,26 @@ function setSelected(el: HTMLSelectElement, value: any) { ) return } + + // fast path for updates triggered by other changes + if (isArrayValue && looseEqual(value, oldValue)) { + return + } + for (let i = 0, l = el.options.length; i < l; i++) { const option = el.options[i] const optionValue = getValue(option) if (isMultiple) { - if (isArray(value)) { - option.selected = looseIndexOf(value, optionValue) > -1 + if (isArrayValue) { + const optionType = typeof optionValue + // fast path for string / number values + if (optionType === 'string' || optionType === 'number') { + option.selected = value.includes( + number ? looseToNumber(optionValue) : optionValue, + ) + } else { + option.selected = looseIndexOf(value, optionValue) > -1 + } } else { option.selected = value.has(optionValue) }