]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-vapor): support merging multiple event handlers on components (#14137)
authoredison <daiwei521@126.com>
Tue, 25 Nov 2025 07:51:35 +0000 (15:51 +0800)
committerGitHub <noreply@github.com>
Tue, 25 Nov 2025 07:51:35 +0000 (15:51 +0800)
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/generators/event.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-vapor/src/dom/event.ts

index dd2d4abb2464979fe83a47da66a48d8fa1e5140b..65b1d3bf011fdd1557038a27369f0822a6771a24 100644 (file)
@@ -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)
index 26a8ee06baaac32d40f50fb8d1edd1cf8b2f2967..c825a4fbb6ec152ed0e1528b5cde8fb0f9b5d3b7 100644 (file)
@@ -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(
         `<Foo @click.foo="a" @click.bar="b" />`,
       )
       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(
+        `<Foo @click.foo="e => a(e)" @click.bar="e => b(e)" />`,
+      )
+      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', () => {
index 93e7f8a6ac0bb46f9745e7267fa5a4aae66fbf0b..b1fe3612c1162bf4f890b0d61c2d71727d92647c 100644 (file)
@@ -56,7 +56,7 @@ export function genCreateComponent(
 
   const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
     (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 */,
         )
index 75e06c224909838b33ea3ebf32b430174040d578..38b7ff2219bf4b23cdab4b88ed65bb2dd30a2518 100644 (file)
@@ -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)
index 587bcf997ca1749e49cdcc8925ec52ebda1cc28b..dc9c7e0aae04d197a54e124d639fb20e5a077349 100644 (file)
@@ -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
index 90f7d60ed2262296c7c35a2bd181d56892f15db9..9dc2c8d0e7e1749c5640f6c65bf914dbc98c074a 100644 (file)
@@ -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)
+      })
+    }
   }
 }