From c7d6a19c8d5e755546ab49dd7f84cfa1b9f22d2f Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 11 Oct 2024 16:05:55 +0800 Subject: [PATCH 1/6] fix(vModel): avoid updating the checkbox within the side effects of the click --- packages/runtime-dom/src/directives/vModel.ts | 6 +- packages/runtime-dom/src/modules/events.ts | 1 + packages/vue/__tests__/e2e/vModel.spec.ts | 62 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 packages/vue/__tests__/e2e/vModel.spec.ts diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 5a27b245a66..cd14c1d9177 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -163,8 +163,10 @@ function setChecked( { value }: DirectiveBinding, vnode: VNode, ) { - // store the v-model value on the element so it can be accessed by the - // change listener. + // avoid updating with clicking on checkbox + if ((el as any)._currentEventType === 'click') + return // store the v-model value on the element so it can be accessed by the + // change listener. ;(el as any)._modelValue = value let checked: boolean diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index 600b0840cde..176228029bd 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -95,6 +95,7 @@ function createInvoker( instance: ComponentInternalInstance | null, ) { const invoker: Invoker = (e: Event & { _vts?: number }) => { + ;(e.currentTarget as any)._currentEventType = e.type // async edge case vuejs/vue#6566 // inner click event triggers patch, event handler // attached to outer element during patch, and triggered again. This diff --git a/packages/vue/__tests__/e2e/vModel.spec.ts b/packages/vue/__tests__/e2e/vModel.spec.ts new file mode 100644 index 00000000000..b767b6caa02 --- /dev/null +++ b/packages/vue/__tests__/e2e/vModel.spec.ts @@ -0,0 +1,62 @@ +import path from 'node:path' +import { setupPuppeteer } from './e2eUtils' + +const { page, click, isChecked, html } = setupPuppeteer() +import { nextTick } from 'vue' + +beforeEach(async () => { + await page().addScriptTag({ + path: path.resolve(__dirname, '../../dist/vue.global.js'), + }) + await page().setContent(`
`) +}) + +// #8638 +test('checkbox click with v-model array', 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 "`, + ) +}) From 199864f7662d5b1211a6ee5b4ac391bb22838d5f Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 11 Oct 2024 16:08:01 +0800 Subject: [PATCH 2/6] test: add more test --- packages/vue/__tests__/e2e/vModel.spec.ts | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/vue/__tests__/e2e/vModel.spec.ts b/packages/vue/__tests__/e2e/vModel.spec.ts index b767b6caa02..0c4570ca9be 100644 --- a/packages/vue/__tests__/e2e/vModel.spec.ts +++ b/packages/vue/__tests__/e2e/vModel.spec.ts @@ -11,6 +11,51 @@ beforeEach(async () => { await page().setContent(`
`) }) +// #12144 +test('checkbox click with v-model', async () => { + await page().evaluate(() => { + const { createApp } = (window as any).Vue + createApp({ + template: ` + +
+ + `, + data() { + return { + first: true, + second: false, + } + }, + methods: { + secondClick(this: any) { + this.first = false + }, + }, + }).mount('#app') + }) + + expect(await isChecked('#first')).toBe(true) + expect(await isChecked('#second')).toBe(false) + await click('#second') + await nextTick() + expect(await isChecked('#first')).toBe(false) + expect(await isChecked('#second')).toBe(true) +}) + // #8638 test('checkbox click with v-model array', async () => { await page().evaluate(() => { From 7dc80c836d5af539ec381e1dc8e835249e7987db Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 11 Oct 2024 16:09:06 +0800 Subject: [PATCH 3/6] test: update test --- packages/vue/__tests__/e2e/vModel.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue/__tests__/e2e/vModel.spec.ts b/packages/vue/__tests__/e2e/vModel.spec.ts index 0c4570ca9be..5caa0f0e6d6 100644 --- a/packages/vue/__tests__/e2e/vModel.spec.ts +++ b/packages/vue/__tests__/e2e/vModel.spec.ts @@ -12,7 +12,7 @@ beforeEach(async () => { }) // #12144 -test('checkbox click with v-model', async () => { +test('checkbox click with v-model boolean value', async () => { await page().evaluate(() => { const { createApp } = (window as any).Vue createApp({ @@ -57,7 +57,7 @@ test('checkbox click with v-model', async () => { }) // #8638 -test('checkbox click with v-model array', async () => { +test('checkbox click with v-model array value', async () => { await page().evaluate(() => { const { createApp, ref } = (window as any).Vue createApp({ From 388ea72961f8975fe2389f88478f40dd1c4595fd Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 11 Oct 2024 16:11:08 +0800 Subject: [PATCH 4/6] chore: format --- packages/runtime-dom/src/directives/vModel.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index cd14c1d9177..9af1850cfd1 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -164,9 +164,11 @@ function setChecked( vnode: VNode, ) { // avoid updating with clicking on checkbox - if ((el as any)._currentEventType === 'click') - return // store the v-model value on the element so it can be accessed by the - // change listener. + if ((el as any)._currentEventType === 'click') { + return + } + // store the v-model value on the element so it can be accessed by the + // change listener. ;(el as any)._modelValue = value let checked: boolean From bf19beb8402154c6803b235833da308ac1c1b4ba Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 11 Oct 2024 16:12:30 +0800 Subject: [PATCH 5/6] chore: update --- packages/runtime-dom/src/directives/vModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 9af1850cfd1..4b2ccb4d85d 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -163,7 +163,7 @@ function setChecked( { value }: DirectiveBinding, vnode: VNode, ) { - // avoid updating with clicking on checkbox + // avoid updating when click event has side effect if ((el as any)._currentEventType === 'click') { return } From 24ee71f61f65aff1d2bfbe7d4c3dd777de079a2e Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 11 Oct 2024 17:53:34 +0800 Subject: [PATCH 6/6] chore: fix #8579 --- packages/runtime-dom/src/directives/vModel.ts | 16 +++++++++++++--- packages/runtime-dom/src/modules/events.ts | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 4b2ccb4d85d..ca2d8593307 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -6,7 +6,7 @@ import { nextTick, warn, } from '@vue/runtime-core' -import { addEventListener } from '../modules/events' +import { addEventListener, globelEvent } from '../modules/events' import { invokeArrayFns, isArray, @@ -163,8 +163,11 @@ function setChecked( { value }: DirectiveBinding, vnode: VNode, ) { - // avoid updating when click event has side effect - if ((el as any)._currentEventType === 'click') { + if ( + globelEvent && + globelEvent.target === el && + globelEvent.type !== 'change' + ) { return } // store the v-model value on the element so it can be accessed by the @@ -243,6 +246,13 @@ export const vModelSelect: ModelDirective = { } function setSelected(el: HTMLSelectElement, value: any) { + if ( + globelEvent && + globelEvent.target === el && + globelEvent.type !== 'change' + ) { + return + } const isMultiple = el.multiple const isArrayValue = isArray(value) if (isMultiple && !isArrayValue && !isSet(value)) { diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index 176228029bd..09f03c24e4e 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -90,12 +90,14 @@ const p = /*@__PURE__*/ Promise.resolve() const getNow = () => cachedNow || (p.then(() => (cachedNow = 0)), (cachedNow = Date.now())) +export let globelEvent: Event | undefined + function createInvoker( initialValue: EventValue, instance: ComponentInternalInstance | null, ) { const invoker: Invoker = (e: Event & { _vts?: number }) => { - ;(e.currentTarget as any)._currentEventType = e.type + globelEvent = e // async edge case vuejs/vue#6566 // inner click event triggers patch, event handler // attached to outer element during patch, and triggered again. This