feat(compiler-core): create transform for v-model (#146)
This commit is contained in:
parent
99bdc5a8c8
commit
87c3d2edae
@ -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 */))
|
||||
}
|
||||
}"
|
||||
`;
|
355
packages/compiler-core/__tests__/transforms/vModel.spec.ts
Normal file
355
packages/compiler-core/__tests__/transforms/vModel.spec.ts
Normal 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
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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.`,
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user