feat(compiler): v-on inline statement handling

This commit is contained in:
Evan You 2019-10-03 17:47:00 -04:00
parent 3354837ce1
commit 2e2b6924da
4 changed files with 114 additions and 10 deletions

View File

@ -121,6 +121,73 @@ describe('compiler: transform v-on', () => {
}) })
}) })
test('should wrap as function if expression is inline statement', () => {
const node = parseWithVOn(`<div @click="i++"/>`)
const props = node.codegenNode!.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `i++` }, `)`]
}
})
})
test('inline statement w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="foo($event)"/>`, {
prefixIdentifiers: true
})
const props = node.codegenNode!.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$event => (`,
{ content: `_ctx.foo` },
`(`,
// should NOT prefix $event
{ content: `$event` },
`)`,
`)`
]
}
})
})
test('should NOT wrap as function if expression is already function expression', () => {
const node = parseWithVOn(`<div @click="$event => foo($event)"/>`)
const props = node.codegenNode!.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `$event => foo($event)`
}
})
})
test('function expression w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
prefixIdentifiers: true
})
const props = node.codegenNode!.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `e` },
` => `,
{ content: `_ctx.foo` },
`(`,
{ content: `e` },
`)`
]
}
})
})
test('should error if no expression AND no modifier', () => { test('should error if no expression AND no modifier', () => {
const onError = jest.fn() const onError = jest.fn()
parseWithVOn(`<div v-on:click />`, { onError }) parseWithVOn(`<div v-on:click />`, { onError })

View File

@ -72,8 +72,8 @@ export interface TransformContext extends Required<TransformOptions> {
replaceNode(node: TemplateChildNode): void replaceNode(node: TemplateChildNode): void
removeNode(node?: TemplateChildNode): void removeNode(node?: TemplateChildNode): void
onNodeRemoved: () => void onNodeRemoved: () => void
addIdentifiers(exp: ExpressionNode): void addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode): void removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: JSChildNode): SimpleExpressionNode hoist(exp: JSChildNode): SimpleExpressionNode
} }
@ -152,7 +152,9 @@ function createTransformContext(
addIdentifiers(exp) { addIdentifiers(exp) {
// identifier tracking only happens in non-browser builds. // identifier tracking only happens in non-browser builds.
if (!__BROWSER__) { if (!__BROWSER__) {
if (exp.identifiers) { if (isString(exp)) {
addId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(addId) exp.identifiers.forEach(addId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
addId(exp.content) addId(exp.content)
@ -161,7 +163,9 @@ function createTransformContext(
}, },
removeIdentifiers(exp) { removeIdentifiers(exp) {
if (!__BROWSER__) { if (!__BROWSER__) {
if (exp.identifiers) { if (isString(exp)) {
removeId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(removeId) exp.identifiers.forEach(removeId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
removeId(exp.content) removeId(exp.content)

View File

@ -35,11 +35,13 @@ export const transformExpression: NodeTransform = (node, context) => {
// handle directives on element // handle directives on element
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const dir = node.props[i] const dir = node.props[i]
// do not process for v-for since it's special handled // do not process for v-on & v-for since they are special handled
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') { if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
const exp = dir.exp as SimpleExpressionNode | undefined const exp = dir.exp as SimpleExpressionNode | undefined
const arg = dir.arg as SimpleExpressionNode | undefined const arg = dir.arg as SimpleExpressionNode | undefined
if (exp) { // do not process exp if this is v-on:arg - we need special handling
// for wrapping inline statements.
if (exp && !(dir.name === 'on' && arg)) {
dir.exp = processExpression( dir.exp = processExpression(
exp, exp,
context, context,

View File

@ -4,18 +4,23 @@ import {
createSimpleExpression, createSimpleExpression,
ExpressionNode, ExpressionNode,
NodeTypes, NodeTypes,
createCompoundExpression createCompoundExpression,
SimpleExpressionNode
} from '../ast' } from '../ast'
import { capitalize } from '@vue/shared' import { capitalize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
// v-on without arg is handled directly in ./element.ts due to it affecting // v-on without arg is handled directly in ./element.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-on // codegen for the entire props object. This transform here is only for v-on
// *with* args. // *with* args.
export const transformOn: DirectiveTransform = (dir, context) => { export const transformOn: DirectiveTransform = (dir, context) => {
const { exp, loc, modifiers } = dir const { loc, modifiers } = dir
const arg = dir.arg! const arg = dir.arg!
if (!exp && !modifiers.length) { if (!dir.exp && !modifiers.length) {
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc)) context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
} }
let eventName: ExpressionNode let eventName: ExpressionNode
@ -37,10 +42,36 @@ export const transformOn: DirectiveTransform = (dir, context) => {
} }
// TODO .once modifier handling since it is platform agnostic // TODO .once modifier handling since it is platform agnostic
// other modifiers are handled in compiler-dom // other modifiers are handled in compiler-dom
// handler processing
if (dir.exp) {
// exp is guarunteed to be a simple expression here because v-on w/ arg is
// skipped by transformExpression as a special case.
let exp: ExpressionNode = dir.exp as SimpleExpressionNode
const isInlineStatement = !(
simplePathRE.test(exp.content) || fnExpRE.test(exp.content)
)
// process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) {
context.addIdentifiers(`$event`)
exp = processExpression(exp, context)
context.removeIdentifiers(`$event`)
}
if (isInlineStatement) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`$event => (`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
`)`
])
}
dir.exp = exp
}
return { return {
props: createObjectProperty( props: createObjectProperty(
eventName, eventName,
exp || createSimpleExpression(`() => {}`, false, loc) dir.exp || createSimpleExpression(`() => {}`, false, loc)
), ),
needRuntime: false needRuntime: false
} }