]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(vapor): v-model and v-model:model co-usage (#13070)
authorzhiyuanzmj <260480378@qq.com>
Thu, 6 Nov 2025 06:22:57 +0000 (14:22 +0800)
committerGitHub <noreply@github.com>
Thu, 6 Nov 2025 06:22:57 +0000 (14:22 +0800)
packages/compiler-core/__tests__/transforms/vModel.spec.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-sfc/src/script/defineModel.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformText.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vModel.spec.ts
packages/compiler-vapor/src/generators/component.ts
packages/runtime-core/src/helpers/useModel.ts
packages/shared/src/general.ts

index 82dd4909fd653e0faecff7db07582cb19841fdf5..10a1952988a593ad73aefb687114d7700a1f87dd 100644 (file)
@@ -507,6 +507,24 @@ describe('compiler: transform v-model', () => {
     )
   })
 
+  test('should generate modelModifiers$ for component v-model:model with arguments', () => {
+    const root = parseWithVModel('<Comp v-model:model.trim="foo" />', {
+      prefixIdentifiers: true,
+    })
+    const vnodeCall = (root.children[0] as ComponentNode)
+      .codegenNode as VNodeCall
+    expect(vnodeCall.props).toMatchObject({
+      properties: [
+        { key: { content: `model` } },
+        { key: { content: `onUpdate:model` } },
+        {
+          key: { content: 'modelModifiers$' },
+          value: { content: `{ trim: true }`, isStatic: false },
+        },
+      ],
+    })
+  })
+
   describe('errors', () => {
     test('missing expression', () => {
       const onError = vi.fn()
index 598c1ea4387205012a38dc0606d1926e5baa4820..40e6a86f10f49319aa03f283f7576aa892401eb4 100644 (file)
@@ -18,7 +18,7 @@ import {
 } from '../utils'
 import { IS_REF } from '../runtimeHelpers'
 import { BindingTypes } from '../options'
-import { camelize } from '@vue/shared'
+import { camelize, getModifierPropName } from '@vue/shared'
 
 export const transformModel: DirectiveTransform = (dir, node, context) => {
   const { exp, arg } = dir
@@ -136,7 +136,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
       .join(`, `)
     const modifiersKey = arg
       ? isStaticExp(arg)
-        ? `${arg.content}Modifiers`
+        ? getModifierPropName(arg.content)
         : createCompoundExpression([arg, ' + "Modifiers"'])
       : `modelModifiers`
     props.push(
index a0b5e11fe4f805ef4545e9841ae0a59a5650ff5c..38d06a7bf0167c184bea67b7d5888f3da37932fb 100644 (file)
@@ -3,6 +3,7 @@ import type { ScriptCompileContext } from './context'
 import { inferRuntimeType } from './resolveType'
 import { UNKNOWN_TYPE, isCallOf, toRuntimeTypeString } from './utils'
 import { BindingTypes, unwrapTSNode } from '@vue/compiler-dom'
+import { getModifierPropName } from '@vue/shared'
 
 export const DEFINE_MODEL = 'defineModel'
 
@@ -167,9 +168,7 @@ export function genModelProps(ctx: ScriptCompileContext): string | undefined {
     modelPropsDecl += `\n    ${JSON.stringify(name)}: ${decl},`
 
     // also generate modifiers prop
-    const modifierPropName = JSON.stringify(
-      name === 'modelValue' ? `modelModifiers` : `${name}Modifiers`,
-    )
+    const modifierPropName = JSON.stringify(getModifierPropName(name))
     modelPropsDecl += `\n    ${modifierPropName}: {},`
   }
   return `{${modelPropsDecl}\n  }`
index e1981aac172259703d71df6a7fa4c5510c576964..8dc459666a49760cab40a21e607e28ca121b7392 100644 (file)
@@ -17,7 +17,7 @@ const t0 = _template("<div>2 foo1 1 1 1</div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  const n0 = _child(n1)
+  const n0 = _child(n1, 0)
   return n1
 }"
 `;
index ae1def83001f2b99441afbb5755dbf0f9dad7edf..4f177e27e40269feb0240ca076e4b97d2bf05a34 100644 (file)
@@ -81,6 +81,18 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: vModel transform > component > v-model:model with arguments for component should generate modelModifiers$ 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, { model: () => (_ctx.foo),
+  "onUpdate:model": () => _value => (_ctx.foo = _value),
+  modelModifiers$: () => ({ trim: true }) }, null, true)
+  return n0
+}"
+`;
+
 exports[`compiler: vModel transform > modifiers > .lazy 1`] = `
 "import { applyTextModel as _applyTextModel, template as _template } from 'vue';
 const t0 = _template("<input>", true)
index 51eaa9e02306088f323abf0879d68fdde7a06d2a..23b99ec70194e76d178afdc2c74291e2de150742 100644 (file)
@@ -319,6 +319,28 @@ describe('compiler: vModel transform', () => {
       })
     })
 
+    test('v-model:model with arguments for component should generate modelModifiers$', () => {
+      const { code, ir } = compileWithVModel(
+        '<Comp v-model:model.trim="foo" />',
+      )
+      expect(code).toMatchSnapshot()
+      expect(code).contain(`modelModifiers$: () => ({ trim: true })`)
+      expect(ir.block.dynamic.children[0].operation).toMatchObject({
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        tag: 'Comp',
+        props: [
+          [
+            {
+              key: { content: 'model', isStatic: true },
+              values: [{ content: 'foo', isStatic: false }],
+              model: true,
+              modelModifiers: ['trim'],
+            },
+          ],
+        ],
+      })
+    })
+
     test('v-model with dynamic arguments for component should generate modelModifiers ', () => {
       const { code, ir } = compileWithVModel(
         '<Comp v-model:[foo].trim="foo" v-model:[bar].number="bar" />',
index 38dd701704eb7f9f165907b5125187e3278eff4c..d4bfeefdb61cb320cc374f385bf03e10100e72de 100644 (file)
@@ -1,4 +1,4 @@
-import { camelize, extend, isArray } from '@vue/shared'
+import { camelize, extend, getModifierPropName, isArray } from '@vue/shared'
 import type { CodegenContext } from '../generate'
 import {
   type CreateComponentIRNode,
@@ -257,9 +257,7 @@ function genModelModifiers(
   if (!modelModifiers || !modelModifiers.length) return []
 
   const modifiersKey = key.isStatic
-    ? key.content === 'modelValue'
-      ? [`modelModifiers`]
-      : [`${key.content}Modifiers`]
+    ? [getModifierPropName(key.content)]
     : ['[', ...genExpression(key, context), ' + "Modifiers"]']
 
   const modifiersVal = genDirectiveModifiers(modelModifiers)
index e85edc6e9a7b7c2bee08490aeba5b928a606fba8..f2a8be36a544c93343fb2fbe6a727facee486234 100644 (file)
@@ -1,5 +1,11 @@
 import { type Ref, customRef, ref } from '@vue/reactivity'
-import { EMPTY_OBJ, camelize, hasChanged, hyphenate } from '@vue/shared'
+import {
+  EMPTY_OBJ,
+  camelize,
+  getModifierPropName,
+  hasChanged,
+  hyphenate,
+} from '@vue/shared'
 import type { DefineModelOptions, ModelRef } from '../apiSetupHelpers'
 import {
   type ComponentInternalInstance,
@@ -145,9 +151,9 @@ export const getModelModifiers = (
   modelName: string,
   getter: (props: Record<string, any>, key: string) => any,
 ): Record<string, boolean> | undefined => {
-  return modelName === 'modelValue' || modelName === 'model-value'
-    ? getter(props, 'modelModifiers')
-    : getter(props, `${modelName}Modifiers`) ||
-        getter(props, `${camelize(modelName)}Modifiers`) ||
-        getter(props, `${hyphenate(modelName)}Modifiers`)
+  return (
+    getter(props, getModifierPropName(modelName)) ||
+    getter(props, `${camelize(modelName)}Modifiers`) ||
+    getter(props, `${hyphenate(modelName)}Modifiers`)
+  )
 }
index bf11ba7a2937f3c27995fa8491ebc9ae7c2a2d5b..5a6e7449340fa6d47be0dd03259e5dc4845343c6 100644 (file)
@@ -153,6 +153,18 @@ export const toHandlerKey: <T extends string>(
   },
 )
 
+/**
+ * #13070 When v-model and v-model:model directives are used together,
+ * they will generate the same modelModifiers prop,
+ * so a `$` suffix is added to avoid conflicts.
+ * @private
+ */
+export const getModifierPropName = (name: string): string => {
+  return `${
+    name === 'modelValue' || name === 'model-value' ? 'model' : name
+  }Modifiers${name === 'model' ? '$' : ''}`
+}
+
 // compare whether a value has changed, accounting for NaN.
 export const hasChanged = (value: any, oldValue: any): boolean =>
   !Object.is(value, oldValue)