diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
new file mode 100644
index 00000000000..ea11cf71519
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compiler: transform v-model compound expression (with prefixIdentifiers) 1`] = `
+"import { createVNode, createBlock, openBlock } from \\"vue\\"
+
+export default function render() {
+ const _ctx = this
+ return (openBlock(), createBlock(\\"input\\", {
+ modelValue: _ctx.model[_ctx.index],
+ \\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event)
+ }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+}"
+`;
+
+exports[`compiler: transform v-model compound expression 1`] = `
+"const _Vue = Vue
+
+return function render() {
+ with (this) {
+ const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"input\\", {
+ modelValue: model[index],
+ \\"onUpdate:modelValue\\": $event => (model[index] = $event)
+ }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+ }
+}"
+`;
+
+exports[`compiler: transform v-model simple exprssion (with prefixIdentifiers) 1`] = `
+"import { createVNode, createBlock, openBlock } from \\"vue\\"
+
+export default function render() {
+ const _ctx = this
+ return (openBlock(), createBlock(\\"input\\", {
+ modelValue: _ctx.model,
+ \\"onUpdate:modelValue\\": $event => (_ctx.model = $event)
+ }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+}"
+`;
+
+exports[`compiler: transform v-model simple exprssion 1`] = `
+"const _Vue = Vue
+
+return function render() {
+ with (this) {
+ const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"input\\", {
+ modelValue: model,
+ \\"onUpdate:modelValue\\": $event => (model = $event)
+ }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
+ }
+}"
+`;
+
+exports[`compiler: transform v-model with argument 1`] = `
+"const _Vue = Vue
+
+return function render() {
+ with (this) {
+ const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"input\\", {
+ value: model,
+ \\"onUpdate:value\\": $event => (model = $event)
+ }, null, 8 /* PROPS */, [\\"value\\", \\"onUpdate:value\\"]))
+ }
+}"
+`;
+
+exports[`compiler: transform v-model with dynamic argument (with prefixIdentifiers) 1`] = `
+"import { createVNode, createBlock, openBlock } from \\"vue\\"
+
+export default function render() {
+ const _ctx = this
+ return (openBlock(), createBlock(\\"input\\", {
+ [_ctx.value]: _ctx.model,
+ [\\"onUpdate:\\"+_ctx.value]: $event => (_ctx.model = $event)
+ }, null, 16 /* FULL_PROPS */))
+}"
+`;
+
+exports[`compiler: transform v-model with dynamic argument 1`] = `
+"const _Vue = Vue
+
+return function render() {
+ with (this) {
+ const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"input\\", {
+ [value]: model,
+ [\\"onUpdate:\\"+value]: $event => (model = $event)
+ }, null, 16 /* FULL_PROPS */))
+ }
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts
new file mode 100644
index 00000000000..818076a5092
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts
@@ -0,0 +1,355 @@
+import {
+ parse,
+ transform,
+ generate,
+ ElementNode,
+ ObjectExpression,
+ CompilerOptions,
+ CallExpression
+} from '../../src'
+import { ErrorCodes } from '../../src/errors'
+import { transformModel } from '../../src/transforms/vModel'
+import { transformElement } from '../../src/transforms/transformElement'
+import { transformExpression } from '../../src/transforms/transformExpression'
+
+function parseWithVModel(template: string, options: CompilerOptions = {}) {
+ const ast = parse(template)
+
+ transform(ast, {
+ nodeTransforms: [transformExpression, transformElement],
+ directiveTransforms: {
+ ...options.directiveTransforms,
+ model: transformModel
+ },
+ ...options
+ })
+
+ return ast
+}
+
+describe('compiler: transform v-model', () => {
+ test('simple exprssion', () => {
+ const root = parseWithVModel('')
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: 'modelValue',
+ isStatic: true
+ },
+ value: {
+ content: 'model',
+ isStatic: false
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ content: 'onUpdate:modelValue',
+ isStatic: true
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: 'model',
+ isStatic: false
+ },
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('simple exprssion (with prefixIdentifiers)', () => {
+ const root = parseWithVModel('', {
+ prefixIdentifiers: true
+ })
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: 'modelValue',
+ isStatic: true
+ },
+ value: {
+ content: '_ctx.model',
+ isStatic: false
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ content: 'onUpdate:modelValue',
+ isStatic: true
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: '_ctx.model',
+ isStatic: false
+ },
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
+ })
+
+ test('compound expression', () => {
+ const root = parseWithVModel('')
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: 'modelValue',
+ isStatic: true
+ },
+ value: {
+ content: 'model[index]',
+ isStatic: false
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ content: 'onUpdate:modelValue',
+ isStatic: true
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: 'model[index]',
+ isStatic: false
+ },
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('compound expression (with prefixIdentifiers)', () => {
+ const root = parseWithVModel('', {
+ prefixIdentifiers: true
+ })
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: 'modelValue',
+ isStatic: true
+ },
+ value: {
+ children: [
+ {
+ content: '_ctx.model',
+ isStatic: false
+ },
+ '[',
+ {
+ content: '_ctx.index',
+ isStatic: false
+ },
+ ']'
+ ]
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ content: 'onUpdate:modelValue',
+ isStatic: true
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: '_ctx.model',
+ isStatic: false
+ },
+ '[',
+ {
+ content: '_ctx.index',
+ isStatic: false
+ },
+ ']',
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
+ })
+
+ test('with argument', () => {
+ const root = parseWithVModel('')
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: 'value',
+ isStatic: true
+ },
+ value: {
+ content: 'model',
+ isStatic: false
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ content: 'onUpdate:value',
+ isStatic: true
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: 'model',
+ isStatic: false
+ },
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with dynamic argument', () => {
+ const root = parseWithVModel('')
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: 'value',
+ isStatic: false
+ },
+ value: {
+ content: 'model',
+ isStatic: false
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ children: [
+ {
+ content: 'onUpdate:',
+ isStatic: true
+ },
+ '+',
+ {
+ content: 'value',
+ isStatic: false
+ }
+ ]
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: 'model',
+ isStatic: false
+ },
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with dynamic argument (with prefixIdentifiers)', () => {
+ const root = parseWithVModel('', {
+ prefixIdentifiers: true
+ })
+ const node = root.children[0] as ElementNode
+ const props = ((node.codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+
+ expect(props[0]).toMatchObject({
+ key: {
+ content: '_ctx.value',
+ isStatic: false
+ },
+ value: {
+ content: '_ctx.model',
+ isStatic: false
+ }
+ })
+
+ expect(props[1]).toMatchObject({
+ key: {
+ children: [
+ {
+ content: 'onUpdate:',
+ isStatic: true
+ },
+ '+',
+ {
+ content: '_ctx.value',
+ isStatic: false
+ }
+ ]
+ },
+ value: {
+ children: [
+ '$event => (',
+ {
+ content: '_ctx.model',
+ isStatic: false
+ },
+ ' = $event)'
+ ]
+ }
+ })
+
+ expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
+ })
+
+ describe('errors', () => {
+ test('missing expression', () => {
+ const onError = jest.fn()
+ parseWithVModel('', { onError })
+
+ expect(onError).toHaveBeenCalledTimes(1)
+ expect(onError).toHaveBeenCalledWith(
+ expect.objectContaining({
+ code: ErrorCodes.X_V_MODEL_NO_EXPRESSION
+ })
+ )
+ })
+
+ test('empty expression', () => {
+ const onError = jest.fn()
+ parseWithVModel('', { onError })
+
+ expect(onError).toHaveBeenCalledTimes(1)
+ expect(onError).toHaveBeenCalledWith(
+ expect.objectContaining({
+ code: ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION
+ })
+ )
+ })
+ })
+})
diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts
index b5d1bea04c9..417d0d18534 100644
--- a/packages/compiler-core/__tests__/utils.spec.ts
+++ b/packages/compiler-core/__tests__/utils.spec.ts
@@ -1,5 +1,9 @@
-import { Position } from '../src/ast'
-import { getInnerRange, advancePositionWithClone } from '../src/utils'
+import { Position, NodeTypes } from '../src/ast'
+import {
+ getInnerRange,
+ advancePositionWithClone,
+ isEmptyExpression
+} from '../src/utils'
function p(line: number, column: number, offset: number): Position {
return { column, line, offset }
@@ -67,3 +71,38 @@ describe('getInnerRange', () => {
expect(loc2.end.offset).toBe(7)
})
})
+
+describe('isEmptyExpression', () => {
+ test('empty', () => {
+ expect(
+ isEmptyExpression({
+ content: '',
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ isStatic: true,
+ loc: null as any
+ })
+ ).toBe(true)
+ })
+
+ test('spaces', () => {
+ expect(
+ isEmptyExpression({
+ content: ' \t ',
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ isStatic: true,
+ loc: null as any
+ })
+ ).toBe(true)
+ })
+
+ test('identifier', () => {
+ expect(
+ isEmptyExpression({
+ content: 'foo',
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ isStatic: true,
+ loc: null as any
+ })
+ ).toBe(false)
+ })
+})
diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts
index 8bd0089f91f..759f34579d1 100644
--- a/packages/compiler-core/src/errors.ts
+++ b/packages/compiler-core/src/errors.ts
@@ -79,6 +79,8 @@ export const enum ErrorCodes {
X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
X_V_SLOT_MISPLACED,
+ X_V_MODEL_NO_EXPRESSION,
+ X_V_MODEL_MALFORMED_EXPRESSION,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
@@ -167,6 +169,8 @@ export const errorMessages: { [code: number]: string } = {
`Extraneous children found when component has explicit slots. ` +
`These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or tags.`,
+ [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
+ [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model has invalid expression.`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts
index 70b786d12ed..3be84226932 100644
--- a/packages/compiler-core/src/transforms/vModel.ts
+++ b/packages/compiler-core/src/transforms/vModel.ts
@@ -1 +1,54 @@
-// TODO
+import { DirectiveTransform } from '../transform'
+import {
+ createSimpleExpression,
+ createObjectProperty,
+ createCompoundExpression,
+ NodeTypes,
+ Property
+} from '../ast'
+import { createCompilerError, ErrorCodes } from '../errors'
+import { isEmptyExpression } from '../utils'
+
+export const transformModel: DirectiveTransform = (dir, node, context) => {
+ const { exp, arg } = dir
+ if (!exp) {
+ context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION))
+
+ return createTransformProps()
+ }
+
+ if (isEmptyExpression(exp)) {
+ context.onError(
+ createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION)
+ )
+
+ return createTransformProps()
+ }
+
+ const propName = arg ? arg : createSimpleExpression('modelValue', true)
+ const eventName = arg
+ ? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
+ ? createSimpleExpression('onUpdate:' + arg.content, true)
+ : createCompoundExpression([
+ createSimpleExpression('onUpdate:', true),
+ '+',
+ ...(arg.type === NodeTypes.SIMPLE_EXPRESSION ? [arg] : arg.children)
+ ])
+ : createSimpleExpression('onUpdate:modelValue', true)
+
+ return createTransformProps([
+ createObjectProperty(propName, dir.exp!),
+ createObjectProperty(
+ eventName,
+ createCompoundExpression([
+ `$event => (`,
+ ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
+ ` = $event)`
+ ])
+ )
+ ])
+}
+
+function createTransformProps(props: Property[] = []) {
+ return { props, needRuntime: false }
+}
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index 83a71bb8f79..f6f28c684fb 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -20,7 +20,8 @@ import {
BlockCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
- ComponentCodegenNode
+ ComponentCodegenNode,
+ ExpressionNode
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
@@ -237,3 +238,7 @@ export function toValidAssetId(
): string {
return `_${type}_${name.replace(/[^\w]/g, '')}`
}
+
+export function isEmptyExpression(node: ExpressionNode) {
+ return node.type === NodeTypes.SIMPLE_EXPRESSION && !node.content.trim()
+}