diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 5057e16d472..64d589ac56c 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -160,12 +160,20 @@ export const vModelCheckbox: ModelDirective = { function setChecked( el: HTMLInputElement, - { value, oldValue }: DirectiveBinding, + { value }: DirectiveBinding, vnode: VNode, ) { + if (looseEqual(value, (el as any)._cachedValue)) { + return + } // store the v-model value on the element so it can be accessed by the // change listener. ;(el as any)._modelValue = value + ;(el as any)._cachedValue = isArray(value) + ? [...value] + : isSet(value) + ? new Set(value) + : value let checked: boolean if (isArray(value)) { @@ -173,7 +181,6 @@ function setChecked( } else if (isSet(value)) { checked = value.has(vnode.props!.value) } else { - if (value === oldValue) return checked = looseEqual(value, getCheckboxValue(el, true)) } @@ -210,13 +217,14 @@ export const vModelSelect: ModelDirective = { .map((o: HTMLOptionElement) => number ? looseToNumber(getValue(o)) : getValue(o), ) - el[assignKey]( - el.multiple - ? isSetModel - ? new Set(selectedVal) - : selectedVal - : selectedVal[0], - ) + const modelValue = el.multiple + ? isSetModel + ? new Set(selectedVal) + : selectedVal + : selectedVal[0] + el[assignKey](modelValue) + ;(el as any)._cachedValue = isArray(value) ? [...value] : value + el._assigning = true nextTick(() => { el._assigning = false @@ -240,6 +248,15 @@ export const vModelSelect: ModelDirective = { } function setSelected(el: HTMLSelectElement, value: any) { + if ((el as any)._assigning && looseEqual(value, (el as any)._cachedValue)) { + return + } + ;(el as any)._cachedValue = isArray(value) + ? [...value] + : isSet(value) + ? new Set(value) + : value + const isMultiple = el.multiple const isArrayValue = isArray(value) if (isMultiple && !isArrayValue && !isSet(value)) { diff --git a/packages/shared/src/looseEqual.ts b/packages/shared/src/looseEqual.ts index 9e71767219c..54f222dc044 100644 --- a/packages/shared/src/looseEqual.ts +++ b/packages/shared/src/looseEqual.ts @@ -1,4 +1,4 @@ -import { isArray, isDate, isObject, isSymbol } from './general' +import { isArray, isDate, isObject, isSet, isSymbol } from './general' function looseCompareArrays(a: any[], b: any[]) { if (a.length !== b.length) return false @@ -9,6 +9,17 @@ function looseCompareArrays(a: any[], b: any[]) { return equal } +function looseCompareSets(a: Set, b: Set) { + if (a.size !== b.size) return false + let equal = true + a.forEach((v: any) => { + if (!b.has(v)) { + equal = false + } + }) + return equal +} + export function looseEqual(a: any, b: any): boolean { if (a === b) return true let aValidType = isDate(a) @@ -26,6 +37,13 @@ export function looseEqual(a: any, b: any): boolean { if (aValidType || bValidType) { return aValidType && bValidType ? looseCompareArrays(a, b) : false } + + aValidType = isSet(a) + bValidType = isSet(b) + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareSets(a, b) : false + } + aValidType = isObject(a) bValidType = isObject(b) if (aValidType || bValidType) { diff --git a/packages/vue/__tests__/e2e/vModel.spec.ts b/packages/vue/__tests__/e2e/vModel.spec.ts index e1a06bda532..598379d7143 100644 --- a/packages/vue/__tests__/e2e/vModel.spec.ts +++ b/packages/vue/__tests__/e2e/vModel.spec.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { setupPuppeteer } from './e2eUtils' -const { page, click, isChecked } = setupPuppeteer() +const { page, click, isChecked, html, value } = setupPuppeteer() import { nextTick } from 'vue' beforeEach(async () => { @@ -55,3 +55,94 @@ test('checkbox click with v-model', async () => { expect(await isChecked('#first')).toBe(false) expect(await isChecked('#second')).toBe(true) }) + +// #8638 +test('checkbox click with v-model array value', async () => { + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + createApp({ + template: ` + {{cls}} + + `, + setup() { + const inputModel = ref([]) + const count = ref(0) + const change = () => { + count.value++ + } + return { + inputModel, + change, + cls: count, + } + }, + }).mount('#app') + }) + + expect(await isChecked('#checkEl')).toBe(false) + expect(await html('#app')).toMatchInlineSnapshot( + `"0 "`, + ) + + await click('#checkEl') + await nextTick() + expect(await isChecked('#checkEl')).toBe(true) + expect(await html('#app')).toMatchInlineSnapshot( + `"1 "`, + ) + + await click('#checkEl') + await nextTick() + expect(await isChecked('#checkEl')).toBe(false) + expect(await html('#app')).toMatchInlineSnapshot( + `"2 "`, + ) +}) + +// #8579 +test('select click with v-model', async () => { + await page().evaluate(() => { + const { createApp } = (window as any).Vue + createApp({ + template: ` +

+ Changed: {{changed}} +

+

+ Chosen: {{chosen}} +

+
+ +
+ `, + data() { + return { + choices: ['A', 'B'], + chosen: 'A', + changed: false, + } + }, + }).mount('#app') + }) + + expect(await value('#selectEl')).toBe('A') + expect(await html('#app')).toMatchInlineSnapshot( + `"

Changed: false

Chosen: A

"`, + ) + + await page().select('#selectEl', 'B') + await nextTick() + expect(await value('#selectEl')).toBe('B') + expect(await html('#app')).toMatchInlineSnapshot( + `"

Changed: true

Chosen: B

"`, + ) +})