diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts index 8e92123d..9a7ba450 100644 --- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@ -121,6 +121,73 @@ describe('compiler: transform v-on', () => { }) }) + test('should wrap as function if expression is inline statement', () => { + const node = parseWithVOn(`
`) + 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(`
`, { + 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(`
`) + 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(`
`, { + 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', () => { const onError = jest.fn() parseWithVOn(`
`, { onError }) diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 370d4f46..588d3e49 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -72,8 +72,8 @@ export interface TransformContext extends Required { replaceNode(node: TemplateChildNode): void removeNode(node?: TemplateChildNode): void onNodeRemoved: () => void - addIdentifiers(exp: ExpressionNode): void - removeIdentifiers(exp: ExpressionNode): void + addIdentifiers(exp: ExpressionNode | string): void + removeIdentifiers(exp: ExpressionNode | string): void hoist(exp: JSChildNode): SimpleExpressionNode } @@ -152,7 +152,9 @@ function createTransformContext( addIdentifiers(exp) { // identifier tracking only happens in non-browser builds. if (!__BROWSER__) { - if (exp.identifiers) { + if (isString(exp)) { + addId(exp) + } else if (exp.identifiers) { exp.identifiers.forEach(addId) } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { addId(exp.content) @@ -161,7 +163,9 @@ function createTransformContext( }, removeIdentifiers(exp) { if (!__BROWSER__) { - if (exp.identifiers) { + if (isString(exp)) { + removeId(exp) + } else if (exp.identifiers) { exp.identifiers.forEach(removeId) } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { removeId(exp.content) diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 36c37833..a88b28a9 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -35,11 +35,13 @@ export const transformExpression: NodeTransform = (node, context) => { // handle directives on element for (let i = 0; i < node.props.length; 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') { const exp = dir.exp 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( exp, context, diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 2c89038f..a28ea544 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -4,18 +4,23 @@ import { createSimpleExpression, ExpressionNode, NodeTypes, - createCompoundExpression + createCompoundExpression, + SimpleExpressionNode } from '../ast' import { capitalize } from '@vue/shared' 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 // codegen for the entire props object. This transform here is only for v-on // *with* args. export const transformOn: DirectiveTransform = (dir, context) => { - const { exp, loc, modifiers } = dir + const { loc, modifiers } = dir const arg = dir.arg! - if (!exp && !modifiers.length) { + if (!dir.exp && !modifiers.length) { context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc)) } let eventName: ExpressionNode @@ -37,10 +42,36 @@ export const transformOn: DirectiveTransform = (dir, context) => { } // TODO .once modifier handling since it is platform agnostic // 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 { props: createObjectProperty( eventName, - exp || createSimpleExpression(`() => {}`, false, loc) + dir.exp || createSimpleExpression(`() => {}`, false, loc) ), needRuntime: false }