Skip to content

Commit a4dc8aa

Browse files
committed
fix: simplify event delegation logic
Only delegate event attributes, and don't take into account bindings/actions while determining that. Never delegate `on:` directives. This simplifies the logic and makes it easier to explain, while avoiding any accidental breaking changes when upgrading from Svelte 4 to 5 without changing code. Fixes #10165 Related to #9714
1 parent a26012f commit a4dc8aa

File tree

6 files changed

+26
-96
lines changed

6 files changed

+26
-96
lines changed

.changeset/rich-cobras-exist.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: simplify event delegation logic, only delegate event attributes

packages/svelte/src/compiler/phases/1-parse/state/element.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ export default function tag(parser) {
128128
fragment: create_fragment(true),
129129
metadata: {
130130
svg: false,
131-
has_spread: false,
132-
can_delegate_events: null
131+
has_spread: false
133132
},
134133
parent: null
135134
}

packages/svelte/src/compiler/phases/2-analyze/index.js

+15-78
Original file line numberDiff line numberDiff line change
@@ -59,35 +59,31 @@ function get_component_name(filename) {
5959
}
6060

6161
/**
62-
* @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'> & { type: string }} node
62+
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
63+
* @param {string} event_name
64+
* @param {import('estree').Expression | null} handler
6365
* @param {import('./types').Context} context
6466
* @returns {null | import('#compiler').DelegatedEvent}
6567
*/
66-
function get_delegated_event(node, context) {
67-
const handler = node.expression;
68-
const event_name = node.name;
69-
68+
function get_delegated_event(event_name, handler, context) {
7069
// Handle delegated event handlers. Bail-out if not a delegated event.
71-
if (!handler || node.modifiers.includes('capture') || !DelegatedEvents.includes(event_name)) {
70+
if (!handler || !DelegatedEvents.includes(event_name)) {
7271
return null;
7372
}
73+
7474
// If we are not working with a RegularElement, then bail-out.
7575
const element = context.path.at(-1);
7676
if (element?.type !== 'RegularElement') {
7777
return null;
7878
}
79-
// If element says we can't delegate because we have multiple OnDirectives of the same type, bail-out.
80-
if (!element.metadata.can_delegate_events) {
81-
return null;
82-
}
8379

8480
/** @type {import('#compiler').DelegatedEvent} */
8581
const non_hoistable = { type: 'non-hoistable' };
8682
/** @type {import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression | null} */
8783
let target_function = null;
8884
let binding = null;
8985

90-
if (node.type === 'Attribute' && element.metadata.has_spread) {
86+
if (element.metadata.has_spread) {
9187
// event attribute becomes part of the dynamic spread array
9288
return non_hoistable;
9389
}
@@ -123,8 +119,7 @@ function get_delegated_event(node, context) {
123119
if (element && event_name) {
124120
if (
125121
element.type !== 'RegularElement' ||
126-
!determine_element_spread_and_delegatable(element).metadata.can_delegate_events ||
127-
(element.metadata.has_spread && node.type === 'Attribute') ||
122+
determine_element_spread(element).metadata.has_spread ||
128123
!DelegatedEvents.includes(event_name)
129124
) {
130125
return non_hoistable;
@@ -183,7 +178,8 @@ function get_delegated_event(node, context) {
183178
) {
184179
return non_hoistable;
185180
}
186-
// If we referebnce the index within an each block, then bail-out.
181+
182+
// If we reference the index within an each block, then bail-out.
187183
if (binding !== null && binding.initial?.type === 'EachBlock') {
188184
return non_hoistable;
189185
}
@@ -204,6 +200,7 @@ function get_delegated_event(node, context) {
204200
}
205201
visited_references.add(reference);
206202
}
203+
207204
return { type: 'hoistable', function: target_function };
208205
}
209206

@@ -858,21 +855,9 @@ const common_visitors = {
858855
});
859856

860857
if (is_event_attribute(node)) {
861-
/** @type {string[]} */
862-
const modifiers = [];
863858
const expression = node.value[0].expression;
864859

865-
let name = node.name.slice(2);
866-
867-
if (is_capture_event(name)) {
868-
name = name.slice(0, -7);
869-
modifiers.push('capture');
870-
}
871-
872-
const delegated_event = get_delegated_event(
873-
{ type: node.type, name, expression, modifiers },
874-
context
875-
);
860+
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
876861

877862
if (delegated_event !== null) {
878863
if (delegated_event.type === 'hoistable') {
@@ -1032,18 +1017,6 @@ const common_visitors = {
10321017
)
10331018
};
10341019
},
1035-
OnDirective(node, context) {
1036-
node.metadata = { delegated: null };
1037-
context.next();
1038-
const delegated_event = get_delegated_event(node, context);
1039-
1040-
if (delegated_event !== null) {
1041-
if (delegated_event.type === 'hoistable') {
1042-
delegated_event.function.metadata.hoistable = true;
1043-
}
1044-
node.metadata.delegated = delegated_event;
1045-
}
1046-
},
10471020
ArrowFunctionExpression: function_visitor,
10481021
FunctionExpression: function_visitor,
10491022
FunctionDeclaration: function_visitor,
@@ -1052,7 +1025,7 @@ const common_visitors = {
10521025
node.metadata.svg = true;
10531026
}
10541027

1055-
determine_element_spread_and_delegatable(node);
1028+
determine_element_spread(node);
10561029

10571030
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
10581031
if (
@@ -1110,51 +1083,15 @@ const common_visitors = {
11101083
};
11111084

11121085
/**
1113-
* Check if events on this element can theoretically be delegated. They can if there's no
1114-
* possibility of an OnDirective and an event attribute on the same element, and if there's
1115-
* no OnDirectives of the same type (the latter is a bit too strict because `on:click on:click on:keyup`
1116-
* means that `on:keyup` can be delegated but we gloss over this edge case).
11171086
* @param {import('#compiler').RegularElement} node
11181087
*/
1119-
function determine_element_spread_and_delegatable(node) {
1120-
if (typeof node.metadata.can_delegate_events === 'boolean') {
1121-
return node; // did this already
1122-
}
1123-
1124-
let events = new Map();
1088+
function determine_element_spread(node) {
11251089
let has_spread = false;
1126-
let has_on = false;
1127-
let has_action_or_bind = false;
11281090
for (const attribute of node.attributes) {
1129-
if (
1130-
attribute.type === 'OnDirective' ||
1131-
(attribute.type === 'Attribute' && is_event_attribute(attribute))
1132-
) {
1133-
let event_name = attribute.name;
1134-
if (attribute.type === 'Attribute') {
1135-
event_name = get_attribute_event_name(event_name);
1136-
}
1137-
events.set(event_name, (events.get(event_name) || 0) + 1);
1138-
if (!has_on && attribute.type === 'OnDirective') {
1139-
has_on = true;
1140-
}
1141-
} else if (!has_spread && attribute.type === 'SpreadAttribute') {
1091+
if (!has_spread && attribute.type === 'SpreadAttribute') {
11421092
has_spread = true;
1143-
} else if (
1144-
!has_action_or_bind &&
1145-
((attribute.type === 'BindDirective' && attribute.name !== 'this') ||
1146-
attribute.type === 'UseDirective')
1147-
) {
1148-
has_action_or_bind = true;
11491093
}
11501094
}
1151-
node.metadata.can_delegate_events =
1152-
// Actions/bindings need the old on:-events to fire in order
1153-
!has_action_or_bind &&
1154-
// spreading events means we don't know if there's an event attribute with the same name as an on:-event
1155-
!(has_spread && has_on) &&
1156-
// multiple on:-events/event attributes with the same name
1157-
![...events.values()].some((count) => count > 1);
11581095
node.metadata.has_spread = has_spread;
11591096

11601097
return node;

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,7 @@ function serialize_event_handler(node, { state, visit }) {
13091309

13101310
/**
13111311
* Serializes an event handler function of the `on:` directive or an attribute starting with `on`
1312-
* @param {Pick<import('#compiler').OnDirective, 'name' | 'modifiers' | 'expression' | 'metadata'>} node
1312+
* @param {{name: string; modifiers: string[]; expression: import('estree').Expression | null; delegated?: import('#compiler').DelegatedEvent | null; }} node
13131313
* @param {import('../types.js').ComponentContext} context
13141314
*/
13151315
function serialize_event(node, context) {
@@ -1318,9 +1318,9 @@ function serialize_event(node, context) {
13181318
if (node.expression) {
13191319
let handler = serialize_event_handler(node, context);
13201320
const event_name = node.name;
1321-
const delegated = node.metadata.delegated;
1321+
const delegated = node.delegated;
13221322

1323-
if (delegated !== null) {
1323+
if (delegated != null) {
13241324
let delegated_assignment;
13251325

13261326
if (!state.events.has(event_name)) {
@@ -1415,7 +1415,7 @@ function serialize_event_attribute(node, context) {
14151415
name: event_name,
14161416
expression: node.value[0].expression,
14171417
modifiers,
1418-
metadata: node.metadata
1418+
delegated: node.metadata.delegated
14191419
},
14201420
context
14211421
);

packages/svelte/src/compiler/types/template.d.ts

-9
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,6 @@ export interface OnDirective extends BaseNode {
202202
/** The 'y' in `on:x={y}` */
203203
expression: null | Expression;
204204
modifiers: string[]; // TODO specify
205-
metadata: {
206-
delegated: null | DelegatedEvent;
207-
};
208205
}
209206

210207
export type DelegatedEvent =
@@ -288,12 +285,6 @@ export interface RegularElement extends BaseElement {
288285
svg: boolean;
289286
/** `true` if contains a SpreadAttribute */
290287
has_spread: boolean;
291-
/**
292-
* `true` if events on this element can theoretically be delegated. This doesn't necessarily mean that
293-
* a specific event will be delegated, as there are other factors which affect the final outcome.
294-
* `null` only until it was determined whether this element can be delegated or not.
295-
*/
296-
can_delegate_events: boolean | null;
297288
};
298289
}
299290

packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ export default test({
3131
b2.click();
3232
await Promise.resolve();
3333

34-
assert.ok(
35-
log[0].stack.startsWith('Error:') && log[0].stack.includes('HTMLButtonElement.on_click')
36-
);
34+
assert.ok(log[0].stack.startsWith('Error:') && log[0].stack.includes('HTMLButtonElement.'));
3735
assert.deepEqual(log[1], 1);
3836
}
3937
});

0 commit comments

Comments
 (0)