diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts index 534e5dfe98e..755594d8795 100644 --- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts @@ -5,7 +5,9 @@ import { nextTick, ref, render, + vModelCheckbox, vModelDynamic, + vModelText, withDirectives, } from '@vue/runtime-dom' @@ -1445,4 +1447,111 @@ describe('vModel', () => { expect(inputNum1.value).toBe('1') }) + + // #12460 + it('prevent input value update in mounted hook if value was updated', async () => { + const Comp = defineComponent({ + data() { + return { + val: 'bug', + } + }, + methods: { + onUpdate() { + this.val = 'ok' + }, + }, + render() { + return h('div', null, [ + withDirectives( + h('input', { + 'onUpdate:modelValue': (v: any) => (this.val = v), + type: 'search', + }), + [[vModelText, this.val]], + ), + 'should be ' + this.val, + this.$slots.default!({ onUpdate: this.onUpdate }), + ]) + }, + }) + + const show = ref(false) + const Page = defineComponent({ + render() { + return show.value + ? h(Comp, null, { + default: (attrs: any) => { + attrs.onUpdate() + return h('div') + }, + }) + : h('div') + }, + }) + + render(h(Page), root) + expect(root.innerHTML).toBe('
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe( + '
should be ok
', + ) + const input = root.querySelector('input')! + expect(input.value).toEqual('ok') + }) + + it('prevent checkbox value update in mounted hook if value was updated', async () => { + const Comp = defineComponent({ + data() { + return { + val: false, + } + }, + methods: { + onUpdate() { + this.val = true + }, + }, + render() { + return h('div', null, [ + withDirectives( + h('input', { + 'onUpdate:modelValue': (v: any) => (this.val = v), + type: 'checkbox', + }), + [[vModelCheckbox, this.val]], + ), + 'should be ' + this.val, + this.$slots.default!({ onUpdate: this.onUpdate }), + ]) + }, + }) + + const show = ref(false) + const Page = defineComponent({ + render() { + return show.value + ? h(Comp, null, { + default: (attrs: any) => { + attrs.onUpdate() + return h('div') + }, + }) + : h('div') + }, + }) + + render(h(Page), root) + expect(root.innerHTML).toBe('
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe( + '
should be true
', + ) + const input = root.querySelector('input')! + expect(input.checked).toEqual(true) + }) }) diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 5057e16d472..c87f8435691 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -40,7 +40,7 @@ function onCompositionEnd(e: Event) { const assignKey: unique symbol = Symbol('_assign') type ModelDirective = ObjectDirective< - T & { [assignKey]: AssignerFn; _assigning?: boolean }, + T & { [assignKey]: AssignerFn; _assigning?: boolean; _initialValue?: any }, any, Modifiers > @@ -52,6 +52,7 @@ export const vModelText: ModelDirective< 'trim' | 'number' | 'lazy' > = { created(el, { modifiers: { lazy, trim, number } }, vnode) { + el._initialValue = el.value el[assignKey] = getModelAssigner(vnode) const castToNumber = number || (vnode.props && vnode.props.type === 'number') @@ -83,6 +84,7 @@ export const vModelText: ModelDirective< }, // set value on mounted so it's after min/max for type="range" mounted(el, { value }) { + if (el._initialValue !== el.value) return el.value = value == null ? '' : value }, beforeUpdate( @@ -122,6 +124,7 @@ export const vModelCheckbox: ModelDirective = { deep: true, created(el, _, vnode) { el[assignKey] = getModelAssigner(vnode) + el._initialValue = el.checked addEventListener(el, 'change', () => { const modelValue = (el as any)._modelValue const elementValue = getValue(el) @@ -151,7 +154,10 @@ export const vModelCheckbox: ModelDirective = { }) }, // set initial checked on mount to wait for true-value/false-value - mounted: setChecked, + mounted(el, binding, vnode) { + if (el._initialValue !== el.checked) return + setChecked(el, binding, vnode) + }, beforeUpdate(el, binding, vnode) { el[assignKey] = getModelAssigner(vnode) setChecked(el, binding, vnode)