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(
+ '',
+ )
+ 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(
+ '',
+ )
+ 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)