Skip to content

Commit d17755a

Browse files
trueadmRich-Harris
andauthored
fix: ensure dynamic event handlers are wrapped in a derived (#12563)
* fix: ensure dynamic event handlers are wrapped in a derived * fix test * feedback * more feedback * address feedback * we have .svelte.js files --------- Co-authored-by: Rich Harris <[email protected]>
1 parent d73c5b8 commit d17755a

File tree

8 files changed

+99
-36
lines changed

8 files changed

+99
-36
lines changed

.changeset/bright-colts-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure dynamic event handlers are wrapped in a derived

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

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
714714
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
715715
} else if (attribute.type === 'OnDirective') {
716716
events[attribute.name] ||= [];
717-
let handler = serialize_event_handler(attribute, context);
717+
let handler = serialize_event_handler(attribute, null, context);
718718
if (attribute.modifiers.includes('once')) {
719719
handler = b.call('$.once', handler);
720720
}
@@ -1122,9 +1122,10 @@ function serialize_render_stmt(update) {
11221122
/**
11231123
* Serializes the event handler function of the `on:` directive
11241124
* @param {Pick<import('#compiler').OnDirective, 'name' | 'modifiers' | 'expression'>} node
1125+
* @param {null | { contains_call_expression: boolean; dynamic: boolean; } | null} metadata
11251126
* @param {import('../types.js').ComponentContext} context
11261127
*/
1127-
function serialize_event_handler(node, { state, visit }) {
1128+
function serialize_event_handler(node, metadata, { state, visit }) {
11281129
/** @type {Expression} */
11291130
let handler;
11301131

@@ -1166,10 +1167,33 @@ function serialize_event_handler(node, { state, visit }) {
11661167
handler = /** @type {Expression} */ (visit(handler));
11671168
}
11681169
} else if (
1169-
handler.type === 'CallExpression' ||
1170-
handler.type === 'ConditionalExpression' ||
1171-
handler.type === 'LogicalExpression'
1170+
metadata?.contains_call_expression &&
1171+
!(
1172+
(handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') &&
1173+
handler.metadata.hoistable
1174+
)
11721175
) {
1176+
// Create a derived dynamic event handler
1177+
const id = b.id(state.scope.generate('event_handler'));
1178+
1179+
state.init.push(
1180+
b.var(id, b.call('$.derived', b.thunk(/** @type {Expression} */ (visit(handler)))))
1181+
);
1182+
1183+
handler = b.function(
1184+
null,
1185+
[b.rest(b.id('$$args'))],
1186+
b.block([
1187+
b.return(
1188+
b.call(
1189+
b.member(b.call('$.get', id), b.id('apply'), false, true),
1190+
b.this,
1191+
b.id('$$args')
1192+
)
1193+
)
1194+
])
1195+
);
1196+
} else if (handler.type === 'ConditionalExpression' || handler.type === 'LogicalExpression') {
11731197
handler = dynamic_handler();
11741198
} else {
11751199
handler = /** @type {Expression} */ (visit(handler));
@@ -1206,17 +1230,18 @@ function serialize_event_handler(node, { state, visit }) {
12061230

12071231
/**
12081232
* Serializes an event handler function of the `on:` directive or an attribute starting with `on`
1209-
* @param {{name: string; modifiers: string[]; expression: Expression | null; delegated?: import('#compiler').DelegatedEvent | null; }} node
1233+
* @param {{name: string;modifiers: string[];expression: Expression | null;delegated?: import('#compiler').DelegatedEvent | null;}} node
1234+
* @param {null | { contains_call_expression: boolean; dynamic: boolean; }} metadata
12101235
* @param {import('../types.js').ComponentContext} context
12111236
*/
1212-
function serialize_event(node, context) {
1237+
function serialize_event(node, metadata, context) {
12131238
const state = context.state;
12141239

12151240
/** @type {Statement} */
12161241
let statement;
12171242

12181243
if (node.expression) {
1219-
let handler = serialize_event_handler(node, context);
1244+
let handler = serialize_event_handler(node, metadata, context);
12201245
const event_name = node.name;
12211246
const delegated = node.delegated;
12221247

@@ -1285,7 +1310,12 @@ function serialize_event(node, context) {
12851310
statement = b.stmt(b.call('$.event', ...args));
12861311
} else {
12871312
statement = b.stmt(
1288-
b.call('$.event', b.literal(node.name), state.node, serialize_event_handler(node, context))
1313+
b.call(
1314+
'$.event',
1315+
b.literal(node.name),
1316+
state.node,
1317+
serialize_event_handler(node, metadata, context)
1318+
)
12891319
);
12901320
}
12911321

@@ -1323,6 +1353,7 @@ function serialize_event_attribute(node, context) {
13231353
modifiers,
13241354
delegated: node.metadata.delegated
13251355
},
1356+
!Array.isArray(node.value) && node.value?.type === 'ExpressionTag' ? node.value.metadata : null,
13261357
context
13271358
);
13281359
}
@@ -2797,7 +2828,7 @@ export const template_visitors = {
27972828
context.next({ ...context.state, in_constructor: false });
27982829
},
27992830
OnDirective(node, context) {
2800-
serialize_event(node, context);
2831+
serialize_event(node, null, context);
28012832
},
28022833
UseDirective(node, { state, next, visit }) {
28032834
const params = [b.id('$$node')];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: '<button>Click</button>',
5+
6+
test({ assert, logs, target }) {
7+
const button = target.querySelector('button');
8+
9+
button?.click();
10+
button?.click();
11+
button?.click();
12+
13+
assert.deepEqual(logs, ['create', 'trigger', 'trigger', 'trigger']);
14+
}
15+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
let makeHandler = null;
3+
makeHandler = () => {
4+
console.log('create');
5+
return () => console.log('trigger');
6+
};
7+
</script>
8+
9+
<button on:click={makeHandler()}>Click</button>
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
import { test } from '../../test';
2-
import { log, handler, log_a } from './event.js';
32

43
export default test({
5-
before_test() {
6-
log.length = 0;
7-
handler.value = log_a;
8-
},
9-
10-
async test({ assert, target }) {
11-
const [b1, b2] = target.querySelectorAll('button');
4+
async test({ assert, logs, target, component }) {
5+
const [b1, b2, b3] = target.querySelectorAll('button');
126

137
b1?.click();
14-
assert.deepEqual(log, ['a']);
8+
assert.deepEqual(logs, ['a']);
159

1610
b2?.click();
1711
b1?.click();
18-
assert.deepEqual(log, ['a', 'b']);
12+
13+
b3?.click();
14+
b1?.click();
15+
16+
assert.deepEqual(logs, ['a', 'b', 'a']);
1917
}
2018
});

packages/svelte/tests/runtime-runes/samples/event-attribute-import/event.js

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const log_a = () => {
2+
console.log('a');
3+
};
4+
5+
export const log_b = () => {
6+
console.log('b');
7+
};
8+
9+
let handle = $state(log_a);
10+
11+
export const handler = {
12+
get value() {
13+
return handle;
14+
},
15+
set value(v) {
16+
handle = v;
17+
}
18+
};
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
2-
import {handler, log_b} from './event.js';
2+
import { handler, log_a, log_b } from './event.svelte.js';
33
</script>
44

55
<button onclick={handler.value}>click</button>
6-
<button onclick={() => handler.value = log_b}>change</button>
6+
<button onclick={() => (handler.value = log_b)}>change</button>
7+
<button onclick={() => (handler.value = log_a)}>change back</button>

0 commit comments

Comments
 (0)