fix(vModel): avoid updates caused by side effects of the click

This commit is contained in:
daiwei 2024-10-12 11:42:30 +08:00
parent 770ea67a9c
commit 3ee62bae0e
2 changed files with 107 additions and 3 deletions

View 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)
}
},

View 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>"`,
)
})