Skip to content

Commit f2152d2

Browse files
authored
fix(compiler-vapor): support merging multiple event handlers on components (#14137)
1 parent 8e83197 commit f2152d2

File tree

6 files changed

+99
-66
lines changed

6 files changed

+99
-66
lines changed

packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,28 @@ export function render(_ctx) {
7575
}"
7676
`;
7777
78+
exports[`compiler: element transform > component > props merging: event handlers 1`] = `
79+
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
80+
81+
export function render(_ctx) {
82+
const _component_Foo = _resolveComponent("Foo")
83+
const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [_ctx.a, _ctx.b] }, null, true)
84+
return n0
85+
}"
86+
`;
87+
88+
exports[`compiler: element transform > component > props merging: inline event handlers 1`] = `
89+
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
90+
91+
export function render(_ctx) {
92+
const _component_Foo = _resolveComponent("Foo")
93+
const _on_click = e => _ctx.a(e)
94+
const _on_click1 = e => _ctx.b(e)
95+
const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [_on_click, _on_click1] }, null, true)
96+
return n0
97+
}"
98+
`;
99+
78100
exports[`compiler: element transform > component > resolve component from setup bindings (inline const) 1`] = `
79101
"
80102
const n0 = _createComponent(Example, null, null, true)

packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -328,26 +328,22 @@ describe('compiler: element transform', () => {
328328
})
329329
})
330330

331-
test.todo('props merging: event handlers', () => {
332-
const { code, ir } = compileWithElementTransform(
331+
test('props merging: event handlers', () => {
332+
const { code } = compileWithElementTransform(
333333
`<Foo @click.foo="a" @click.bar="b" />`,
334334
)
335335
expect(code).toMatchSnapshot()
336336
expect(code).contains('onClick: () => [_ctx.a, _ctx.b]')
337-
expect(ir.block.operation).toMatchObject([
338-
{
339-
type: IRNodeTypes.CREATE_COMPONENT_NODE,
340-
tag: 'Foo',
341-
props: [
342-
[
343-
{
344-
key: { content: 'onClick', isStatic: true },
345-
values: [{ content: 'a' }, { content: 'b' }],
346-
},
347-
],
348-
],
349-
},
350-
])
337+
})
338+
339+
test('props merging: inline event handlers', () => {
340+
const { code } = compileWithElementTransform(
341+
`<Foo @click.foo="e => a(e)" @click.bar="e => b(e)" />`,
342+
)
343+
expect(code).toMatchSnapshot()
344+
expect(code).contains('const _on_click = e => _ctx.a(e)')
345+
expect(code).contains('const _on_click1 = e => _ctx.b(e)')
346+
expect(code).contains('onClick: () => [_on_click, _on_click1]')
351347
})
352348

353349
test.todo('props merging: style', () => {

packages/compiler-vapor/src/generators/component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function genCreateComponent(
5656

5757
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
5858
(acc, { name, value }: InlineHandler) => {
59-
const handler = genEventHandler(context, value, undefined, false)
59+
const handler = genEventHandler(context, [value], undefined, false)
6060
return [...acc, `const ${name} = `, ...handler, NEWLINE]
6161
},
6262
[],
@@ -226,7 +226,7 @@ function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) {
226226
...(prop.handler
227227
? genEventHandler(
228228
context,
229-
prop.values[0],
229+
prop.values,
230230
prop.handlerModifiers,
231231
true /* wrap handlers passed to components */,
232232
)

packages/compiler-vapor/src/generators/event.ts

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function genSetEvent(
3131
const name = genName()
3232
const handler = [
3333
`${context.helper('createInvoker')}(`,
34-
...genEventHandler(context, value, modifiers),
34+
...genEventHandler(context, [value], modifiers),
3535
`)`,
3636
]
3737
const eventOptions = genEventOptions()
@@ -112,57 +112,68 @@ export function genSetDynamicEvents(
112112

113113
export function genEventHandler(
114114
context: CodegenContext,
115-
value: SimpleExpressionNode | undefined,
115+
values: (SimpleExpressionNode | undefined)[] | undefined,
116116
modifiers: {
117117
nonKeys: string[]
118118
keys: string[]
119119
} = { nonKeys: [], keys: [] },
120120
// passed as component prop - need additional wrap
121121
extraWrap: boolean = false,
122122
): CodeFragment[] {
123-
let handlerExp: CodeFragment[] = [`() => {}`]
124-
if (value && value.content.trim()) {
125-
// Determine how the handler should be wrapped so it always reference the
126-
// latest value when invoked.
127-
if (isMemberExpression(value, context.options)) {
128-
// e.g. @click="foo.bar"
129-
handlerExp = genExpression(value, context)
130-
if (!isConstantBinding(value, context) && !extraWrap) {
131-
// non constant, wrap with invocation as `e => foo.bar(e)`
132-
// when passing as component handler, access is always dynamic so we
133-
// can skip this
134-
const isTSNode = value.ast && TS_NODE_TYPES.includes(value.ast.type)
135-
handlerExp = [
136-
`e => `,
137-
isTSNode ? '(' : '',
138-
...handlerExp,
139-
isTSNode ? ')' : '',
140-
`(e)`,
141-
]
123+
let handlerExp: CodeFragment[] = []
124+
if (values) {
125+
values.forEach((value, index) => {
126+
let exp: CodeFragment[] = []
127+
if (value && value.content.trim()) {
128+
// Determine how the handler should be wrapped so it always reference the
129+
// latest value when invoked.
130+
if (isMemberExpression(value, context.options)) {
131+
// e.g. @click="foo.bar"
132+
exp = genExpression(value, context)
133+
if (!isConstantBinding(value, context) && !extraWrap) {
134+
// non constant, wrap with invocation as `e => foo.bar(e)`
135+
// when passing as component handler, access is always dynamic so we
136+
// can skip this
137+
const isTSNode = value.ast && TS_NODE_TYPES.includes(value.ast.type)
138+
exp = [
139+
`e => `,
140+
isTSNode ? '(' : '',
141+
...exp,
142+
isTSNode ? ')' : '',
143+
`(e)`,
144+
]
145+
}
146+
} else if (isFnExpression(value, context.options)) {
147+
// Fn expression: @click="e => foo(e)"
148+
// no need to wrap in this case
149+
exp = genExpression(value, context)
150+
} else {
151+
// inline statement
152+
// @click="foo($event)" ---> $event => foo($event)
153+
const referencesEvent = value.content.includes('$event')
154+
const hasMultipleStatements = value.content.includes(`;`)
155+
const expr = referencesEvent
156+
? context.withId(() => genExpression(value, context), {
157+
$event: null,
158+
})
159+
: genExpression(value, context)
160+
exp = [
161+
referencesEvent ? '$event => ' : '() => ',
162+
hasMultipleStatements ? '{' : '(',
163+
...expr,
164+
hasMultipleStatements ? '}' : ')',
165+
]
166+
}
167+
handlerExp = handlerExp.concat([index !== 0 ? ', ' : '', ...exp])
142168
}
143-
} else if (isFnExpression(value, context.options)) {
144-
// Fn expression: @click="e => foo(e)"
145-
// no need to wrap in this case
146-
handlerExp = genExpression(value, context)
147-
} else {
148-
// inline statement
149-
// @click="foo($event)" ---> $event => foo($event)
150-
const referencesEvent = value.content.includes('$event')
151-
const hasMultipleStatements = value.content.includes(`;`)
152-
const expr = referencesEvent
153-
? context.withId(() => genExpression(value, context), {
154-
$event: null,
155-
})
156-
: genExpression(value, context)
157-
handlerExp = [
158-
referencesEvent ? '$event => ' : '() => ',
159-
hasMultipleStatements ? '{' : '(',
160-
...expr,
161-
hasMultipleStatements ? '}' : ')',
162-
]
169+
})
170+
171+
if (values.length > 1) {
172+
handlerExp = ['[', ...handlerExp, ']']
163173
}
164174
}
165175

176+
if (handlerExp.length === 0) handlerExp = ['() => {}']
166177
const { keys, nonKeys } = modifiers
167178
if (nonKeys.length)
168179
handlerExp = genWithModifiers(context, handlerExp, nonKeys)

packages/compiler-vapor/src/transforms/transformElement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] {
492492
// prop names and event handler names can be the same but serve different purposes
493493
// e.g. `:appear="true"` is a prop while `@appear="handler"` is an event handler
494494
if (existing && existing.handler === prop.handler) {
495-
if (name === 'style' || name === 'class') {
495+
if (name === 'style' || name === 'class' || prop.handler) {
496496
mergePropValues(existing, prop)
497497
}
498498
// unexpected duplicate, should have emitted error during parse

packages/runtime-vapor/src/dom/event.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ export function addEventListener(
1515
export function on(
1616
el: Element,
1717
event: string,
18-
handler: (e: Event) => any,
18+
handler: (e: Event) => any | ((e: Event) => any)[],
1919
options: AddEventListenerOptions & { effect?: boolean } = {},
2020
): void {
21-
addEventListener(el, event, handler, options)
22-
if (options.effect) {
23-
onEffectCleanup(() => {
24-
el.removeEventListener(event, handler, options)
25-
})
21+
if (isArray(handler)) {
22+
handler.forEach(fn => on(el, event, fn, options))
23+
} else {
24+
addEventListener(el, event, handler, options)
25+
if (options.effect) {
26+
onEffectCleanup(() => {
27+
el.removeEventListener(event, handler, options)
28+
})
29+
}
2630
}
2731
}
2832

0 commit comments

Comments
 (0)