diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 5a27b245a66..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,6 +163,13 @@ function setChecked( { value }: DirectiveBinding, vnode: VNode, ) { + if ( + globelEvent && + globelEvent.target === el && + globelEvent.type !== 'change' + ) { + return + } // store the v-model value on the element so it can be accessed by the // change listener. ;(el as any)._modelValue = value @@ -239,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 600b0840cde..09f03c24e4e 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -90,11 +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 }) => { + 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 diff --git a/packages/vue/__tests__/e2e/vModel.spec.ts b/packages/vue/__tests__/e2e/vModel.spec.ts new file mode 100644 index 00000000000..5caa0f0e6d6 --- /dev/null +++ b/packages/vue/__tests__/e2e/vModel.spec.ts @@ -0,0 +1,107 @@ +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(`
`) +}) + +// #12144 +test('checkbox click with v-model boolean value', 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 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 "`, + ) +})