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