diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 17c4e80b160..b8ddb5812ba 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -26,6 +26,21 @@ return function render(_ctx, _cache) { }" `; +exports[`compiler: transform v-model > no expression 1`] = ` +"const _Vue = Vue + +return function render(_ctx, _cache) { + with (_ctx) { + const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue + + return (_openBlock(), _createElementBlock("input", { + "foo-value": fooValue, + "onUpdate:fooValue": $event => ((fooValue) = $event) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["foo-value", "onUpdate:fooValue"])) + } +}" +`; + exports[`compiler: transform v-model > simple expression (with multilines) 1`] = ` "const _Vue = Vue diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts index 82dd4909fd6..6915e7f499d 100644 --- a/packages/compiler-core/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts @@ -394,6 +394,42 @@ describe('compiler: transform v-model', () => { expect(generate(root, { mode: 'module' }).code).toMatchSnapshot() }) + test('no expression', () => { + const root = parseWithVModel('<input v-model:foo-value />') + const node = root.children[0] as ElementNode + const props = ((node.codegenNode as VNodeCall).props as ObjectExpression) + .properties + expect(props[0]).toMatchObject({ + key: { + content: 'foo-value', + isStatic: true, + }, + value: { + content: 'fooValue', + isStatic: false, + }, + }) + + expect(props[1]).toMatchObject({ + key: { + content: 'onUpdate:fooValue', + isStatic: true, + }, + value: { + children: [ + '$event => ((', + { + content: 'fooValue', + isStatic: false, + }, + ') = $event)', + ], + }, + }) + + expect(generate(root).code).toMatchSnapshot() + }) + test('should cache update handler w/ cacheHandlers: true', () => { const root = parseWithVModel('<input v-model="foo" />', { prefixIdentifiers: true, @@ -508,14 +544,26 @@ describe('compiler: transform v-model', () => { }) describe('errors', () => { - test('missing expression', () => { + test('missing argument and expression', () => { const onError = vi.fn() parseWithVModel('<span v-model />', { onError }) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( expect.objectContaining({ - code: ErrorCodes.X_V_MODEL_NO_EXPRESSION, + code: ErrorCodes.X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION, + }), + ) + }) + + test('invalid argument for same-name shorthand', () => { + const onError = vi.fn() + parseWithVModel(`<div v-model:[arg] />`, { onError }) + + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ + code: ErrorCodes.X_V_MODEL_INVALID_SAME_NAME_ARGUMENT, }), ) }) diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index 58e113ab19e..c980fbc7b38 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -84,7 +84,8 @@ export enum ErrorCodes { X_V_SLOT_DUPLICATE_SLOT_NAMES, X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN, X_V_SLOT_MISPLACED, - X_V_MODEL_NO_EXPRESSION, + X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION, + X_V_MODEL_INVALID_SAME_NAME_ARGUMENT, X_V_MODEL_MALFORMED_EXPRESSION, X_V_MODEL_ON_SCOPE_VARIABLE, X_V_MODEL_ON_PROPS, @@ -172,7 +173,8 @@ export const errorMessages: Record<ErrorCodes, string> = { `Extraneous children found when component already has explicitly named ` + `default slot. These children will be ignored.`, [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`, - [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`, + [ErrorCodes.X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION]: `v-model is missing argument and expression.`, + [ErrorCodes.X_V_MODEL_INVALID_SAME_NAME_ARGUMENT]: `v-model with same-name shorthand only allows static argument.`, [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`, [ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`, [ErrorCodes.X_V_MODEL_ON_PROPS]: `v-model cannot be used on a prop, because local prop bindings are not writable.\nUse a v-bind binding combined with a v-on listener that emits update:x event instead.`, diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 598c1ea4387..927627b3097 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -19,14 +19,39 @@ import { import { IS_REF } from '../runtimeHelpers' import { BindingTypes } from '../options' import { camelize } from '@vue/shared' +import { transformBindShorthand } from './vBind' export const transformModel: DirectiveTransform = (dir, node, context) => { - const { exp, arg } = dir + const { arg, loc } = dir + let { exp } = dir + if (!exp) { - context.onError( - createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc), - ) - return createTransformProps() + if (!arg) { + context.onError( + createCompilerError( + ErrorCodes.X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION, + dir.loc, + ), + ) + return createTransformProps() + } + + // same-name shorthand - v-model:arg is expanded to v-model:arg="arg" + if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) { + // only simple expression is allowed for same-name shorthand + context.onError( + createCompilerError( + ErrorCodes.X_V_MODEL_INVALID_SAME_NAME_ARGUMENT, + arg.loc, + ), + ) + return createTransformProps([ + createObjectProperty(arg, createSimpleExpression('', true, loc)), + ]) + } + + transformBindShorthand(dir, context) + exp = dir.exp! } // we assume v-model directives are always parsed diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts index b47624840ab..5bffa4e53a1 100644 --- a/packages/compiler-dom/src/errors.ts +++ b/packages/compiler-dom/src/errors.ts @@ -21,7 +21,7 @@ export function createDOMCompilerError( } export enum DOMErrorCodes { - X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */, + X_V_HTML_NO_EXPRESSION = 54 /* ErrorCodes.__EXTEND_POINT__ */, X_V_HTML_WITH_CHILDREN, X_V_TEXT_NO_EXPRESSION, X_V_TEXT_WITH_CHILDREN,