feat(compiler-core): create transform for v-model (#146)

This commit is contained in:
Rahul Kadyan 2019-10-10 20:03:58 +05:30 committed by Evan You
parent 99bdc5a8c8
commit 87c3d2edae
6 changed files with 557 additions and 4 deletions

View File

@ -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 */))
}
}"
`;

View File

@ -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('<input v-model="model" />')
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('<input v-model="model" />', {
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('<input v-model="model[index]" />')
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('<input v-model="model[index]" />', {
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('<input v-model:value="model" />')
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('<input v-model:[value]="model" />')
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('<input v-model:[value]="model" />', {
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('<span v-model />', { 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('<span v-model="" />', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION
})
)
})
})
})

View File

@ -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)
})
})

View File

@ -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 <template> 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.`,

View File

@ -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 }
}

View File

@ -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()
}