diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index dd2d4abb246..65b1d3bf011 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -75,6 +75,28 @@ export function render(_ctx) { }" `; +exports[`compiler: element transform > component > props merging: event handlers 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [_ctx.a, _ctx.b] }, null, true) + return n0 +}" +`; + +exports[`compiler: element transform > component > props merging: inline event handlers 1`] = ` +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _component_Foo = _resolveComponent("Foo") + const _on_click = e => _ctx.a(e) + const _on_click1 = e => _ctx.b(e) + const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [_on_click, _on_click1] }, null, true) + return n0 +}" +`; + exports[`compiler: element transform > component > resolve component from setup bindings (inline const) 1`] = ` " const n0 = _createComponent(Example, null, null, true) diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index 26a8ee06baa..c825a4fbb6e 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -328,26 +328,22 @@ describe('compiler: element transform', () => { }) }) - test.todo('props merging: event handlers', () => { - const { code, ir } = compileWithElementTransform( + test('props merging: event handlers', () => { + const { code } = compileWithElementTransform( ``, ) expect(code).toMatchSnapshot() expect(code).contains('onClick: () => [_ctx.a, _ctx.b]') - expect(ir.block.operation).toMatchObject([ - { - type: IRNodeTypes.CREATE_COMPONENT_NODE, - tag: 'Foo', - props: [ - [ - { - key: { content: 'onClick', isStatic: true }, - values: [{ content: 'a' }, { content: 'b' }], - }, - ], - ], - }, - ]) + }) + + test('props merging: inline event handlers', () => { + const { code } = compileWithElementTransform( + ``, + ) + expect(code).toMatchSnapshot() + expect(code).contains('const _on_click = e => _ctx.a(e)') + expect(code).contains('const _on_click1 = e => _ctx.b(e)') + expect(code).contains('onClick: () => [_on_click, _on_click1]') }) test.todo('props merging: style', () => { diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 93e7f8a6ac0..b1fe3612c11 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -56,7 +56,7 @@ export function genCreateComponent( const inlineHandlers: CodeFragment[] = handlers.reduce( (acc, { name, value }: InlineHandler) => { - const handler = genEventHandler(context, value, undefined, false) + const handler = genEventHandler(context, [value], undefined, false) return [...acc, `const ${name} = `, ...handler, NEWLINE] }, [], @@ -226,7 +226,7 @@ function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) { ...(prop.handler ? genEventHandler( context, - prop.values[0], + prop.values, prop.handlerModifiers, true /* wrap handlers passed to components */, ) diff --git a/packages/compiler-vapor/src/generators/event.ts b/packages/compiler-vapor/src/generators/event.ts index 75e06c22490..38b7ff2219b 100644 --- a/packages/compiler-vapor/src/generators/event.ts +++ b/packages/compiler-vapor/src/generators/event.ts @@ -31,7 +31,7 @@ export function genSetEvent( const name = genName() const handler = [ `${context.helper('createInvoker')}(`, - ...genEventHandler(context, value, modifiers), + ...genEventHandler(context, [value], modifiers), `)`, ] const eventOptions = genEventOptions() @@ -112,7 +112,7 @@ export function genSetDynamicEvents( export function genEventHandler( context: CodegenContext, - value: SimpleExpressionNode | undefined, + values: (SimpleExpressionNode | undefined)[] | undefined, modifiers: { nonKeys: string[] keys: string[] @@ -120,49 +120,60 @@ export function genEventHandler( // passed as component prop - need additional wrap extraWrap: boolean = false, ): CodeFragment[] { - let handlerExp: CodeFragment[] = [`() => {}`] - if (value && value.content.trim()) { - // Determine how the handler should be wrapped so it always reference the - // latest value when invoked. - if (isMemberExpression(value, context.options)) { - // e.g. @click="foo.bar" - handlerExp = genExpression(value, context) - if (!isConstantBinding(value, context) && !extraWrap) { - // non constant, wrap with invocation as `e => foo.bar(e)` - // when passing as component handler, access is always dynamic so we - // can skip this - const isTSNode = value.ast && TS_NODE_TYPES.includes(value.ast.type) - handlerExp = [ - `e => `, - isTSNode ? '(' : '', - ...handlerExp, - isTSNode ? ')' : '', - `(e)`, - ] + let handlerExp: CodeFragment[] = [] + if (values) { + values.forEach((value, index) => { + let exp: CodeFragment[] = [] + if (value && value.content.trim()) { + // Determine how the handler should be wrapped so it always reference the + // latest value when invoked. + if (isMemberExpression(value, context.options)) { + // e.g. @click="foo.bar" + exp = genExpression(value, context) + if (!isConstantBinding(value, context) && !extraWrap) { + // non constant, wrap with invocation as `e => foo.bar(e)` + // when passing as component handler, access is always dynamic so we + // can skip this + const isTSNode = value.ast && TS_NODE_TYPES.includes(value.ast.type) + exp = [ + `e => `, + isTSNode ? '(' : '', + ...exp, + isTSNode ? ')' : '', + `(e)`, + ] + } + } else if (isFnExpression(value, context.options)) { + // Fn expression: @click="e => foo(e)" + // no need to wrap in this case + exp = genExpression(value, context) + } else { + // inline statement + // @click="foo($event)" ---> $event => foo($event) + const referencesEvent = value.content.includes('$event') + const hasMultipleStatements = value.content.includes(`;`) + const expr = referencesEvent + ? context.withId(() => genExpression(value, context), { + $event: null, + }) + : genExpression(value, context) + exp = [ + referencesEvent ? '$event => ' : '() => ', + hasMultipleStatements ? '{' : '(', + ...expr, + hasMultipleStatements ? '}' : ')', + ] + } + handlerExp = handlerExp.concat([index !== 0 ? ', ' : '', ...exp]) } - } else if (isFnExpression(value, context.options)) { - // Fn expression: @click="e => foo(e)" - // no need to wrap in this case - handlerExp = genExpression(value, context) - } else { - // inline statement - // @click="foo($event)" ---> $event => foo($event) - const referencesEvent = value.content.includes('$event') - const hasMultipleStatements = value.content.includes(`;`) - const expr = referencesEvent - ? context.withId(() => genExpression(value, context), { - $event: null, - }) - : genExpression(value, context) - handlerExp = [ - referencesEvent ? '$event => ' : '() => ', - hasMultipleStatements ? '{' : '(', - ...expr, - hasMultipleStatements ? '}' : ')', - ] + }) + + if (values.length > 1) { + handlerExp = ['[', ...handlerExp, ']'] } } + if (handlerExp.length === 0) handlerExp = ['() => {}'] const { keys, nonKeys } = modifiers if (nonKeys.length) handlerExp = genWithModifiers(context, handlerExp, nonKeys) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 587bcf997ca..dc9c7e0aae0 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -492,7 +492,7 @@ function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] { // prop names and event handler names can be the same but serve different purposes // e.g. `:appear="true"` is a prop while `@appear="handler"` is an event handler if (existing && existing.handler === prop.handler) { - if (name === 'style' || name === 'class') { + if (name === 'style' || name === 'class' || prop.handler) { mergePropValues(existing, prop) } // unexpected duplicate, should have emitted error during parse diff --git a/packages/runtime-vapor/src/dom/event.ts b/packages/runtime-vapor/src/dom/event.ts index 90f7d60ed22..9dc2c8d0e7e 100644 --- a/packages/runtime-vapor/src/dom/event.ts +++ b/packages/runtime-vapor/src/dom/event.ts @@ -15,14 +15,18 @@ export function addEventListener( export function on( el: Element, event: string, - handler: (e: Event) => any, + handler: (e: Event) => any | ((e: Event) => any)[], options: AddEventListenerOptions & { effect?: boolean } = {}, ): void { - addEventListener(el, event, handler, options) - if (options.effect) { - onEffectCleanup(() => { - el.removeEventListener(event, handler, options) - }) + if (isArray(handler)) { + handler.forEach(fn => on(el, event, fn, options)) + } else { + addEventListener(el, event, handler, options) + if (options.effect) { + onEffectCleanup(() => { + el.removeEventListener(event, handler, options) + }) + } } }