]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(vModel): avoid updates caused by side effects of the click
authordaiwei <daiwei521@126.com>
Sat, 12 Oct 2024 03:42:30 +0000 (11:42 +0800)
committerdaiwei <daiwei521@126.com>
Sat, 12 Oct 2024 03:42:30 +0000 (11:42 +0800)
packages/runtime-dom/src/directives/vModel.ts
packages/vue/__tests__/e2e/vModel.spec.ts

index 5057e16d472bc59304b2bb386e96b53934e6cf92..7d93b705d7fc56467a88d73b1ef93e305427d07b 100644 (file)
@@ -122,6 +122,9 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
   deep: true,
   created(el, _, vnode) {
     el[assignKey] = getModelAssigner(vnode)
+    addEventListener(el, 'mousedown', () => {
+      ;(el as any)._willChange = true
+    })
     addEventListener(el, 'change', () => {
       const modelValue = (el as any)._modelValue
       const elementValue = getValue(el)
@@ -153,6 +156,10 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
   // set initial checked on mount to wait for true-value/false-value
   mounted: setChecked,
   beforeUpdate(el, binding, vnode) {
+    if ((el as any)._willChange) {
+      ;(el as any)._willChange = false
+      return
+    }
     el[assignKey] = getModelAssigner(vnode)
     setChecked(el, binding, vnode)
   },
@@ -160,7 +167,7 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
 
 function setChecked(
   el: HTMLInputElement,
-  { value, oldValue }: DirectiveBinding,
+  { value }: DirectiveBinding,
   vnode: VNode,
 ) {
   // store the v-model value on the element so it can be accessed by the
@@ -173,7 +180,6 @@ function setChecked(
   } else if (isSet(value)) {
     checked = value.has(vnode.props!.value)
   } else {
-    if (value === oldValue) return
     checked = looseEqual(value, getCheckboxValue(el, true))
   }
 
@@ -204,6 +210,9 @@ export const vModelSelect: ModelDirective<HTMLSelectElement, 'number'> = {
   deep: true,
   created(el, { value, modifiers: { number } }, vnode) {
     const isSetModel = isSet(value)
+    addEventListener(el, 'mousedown', () => {
+      ;(el as any)._willChange = true
+    })
     addEventListener(el, 'change', () => {
       const selectedVal = Array.prototype.filter
         .call(el.options, (o: HTMLOptionElement) => o.selected)
@@ -234,6 +243,10 @@ export const vModelSelect: ModelDirective<HTMLSelectElement, 'number'> = {
   },
   updated(el, { value }) {
     if (!el._assigning) {
+      if ((el as any)._willChange) {
+        ;(el as any)._willChange = false
+        return
+      }
       setSelected(el, value)
     }
   },
index e1a06bda532bfdb31bc6538ffc6cda8231c8b4ef..598379d7143d9cfc1e41c017263394e2ec4e84aa 100644 (file)
@@ -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}}
+      <input
+        id="checkEl"
+        type="checkbox"
+        @click="change"
+        v-model="inputModel"
+        value="a"
+      >
+        `,
+      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 <input id="checkEl" type="checkbox" value="a">"`,
+  )
+
+  await click('#checkEl')
+  await nextTick()
+  expect(await isChecked('#checkEl')).toBe(true)
+  expect(await html('#app')).toMatchInlineSnapshot(
+    `"1 <input id="checkEl" type="checkbox" value="a">"`,
+  )
+
+  await click('#checkEl')
+  await nextTick()
+  expect(await isChecked('#checkEl')).toBe(false)
+  expect(await html('#app')).toMatchInlineSnapshot(
+    `"2 <input id="checkEl" type="checkbox" value="a">"`,
+  )
+})
+
+// #8579
+test('select click with v-model', async () => {
+  await page().evaluate(() => {
+    const { createApp } = (window as any).Vue
+    createApp({
+      template: `
+        <p>
+          Changed: {{changed}}
+        </p>
+        <p>
+          Chosen: {{chosen}}
+        </p>
+        <form @input="changed = true">
+          <select id="selectEl" v-model="chosen">
+            <option v-for="choice of choices" :key="choice" :value="choice">{{ choice }}</option>
+          </select>
+        </form>
+        `,
+      data() {
+        return {
+          choices: ['A', 'B'],
+          chosen: 'A',
+          changed: false,
+        }
+      },
+    }).mount('#app')
+  })
+
+  expect(await value('#selectEl')).toBe('A')
+  expect(await html('#app')).toMatchInlineSnapshot(
+    `"<p> Changed: false</p><p> Chosen: A</p><form><select id="selectEl"><option value="A">A</option><option value="B">B</option></select></form>"`,
+  )
+
+  await page().select('#selectEl', 'B')
+  await nextTick()
+  expect(await value('#selectEl')).toBe('B')
+  expect(await html('#app')).toMatchInlineSnapshot(
+    `"<p> Changed: true</p><p> Chosen: B</p><form><select id="selectEl"><option value="A">A</option><option value="B">B</option></select></form>"`,
+  )
+})