Skip to content

Commit f73fb4b

Browse files
committed
fix: run event attributes after binding event listeners
By running the event listener logic inside an effect on the first run we guarantee that they're attached after binding listeners. Fixes #11138.
1 parent de2d8a0 commit f73fb4b

File tree

4 files changed

+56
-1
lines changed

4 files changed

+56
-1
lines changed

.changeset/seven-garlics-serve.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: make sure event attributes run after bindings

packages/svelte/src/internal/client/dom/elements/attributes.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { get_descriptors, map_get, map_set, object_assign } from '../../utils.js
44
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
55
import { delegate } from './events.js';
66
import { autofocus } from './misc.js';
7+
import { effect } from '../../reactivity/effects.js';
8+
import { run } from '../../../shared/utils.js';
79

810
/**
911
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@@ -106,6 +108,8 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
106108

107109
// @ts-expect-error
108110
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
111+
/** @type {Array<() => void>} */
112+
var events = [];
109113

110114
for (key in next) {
111115
var value = next[key];
@@ -135,7 +139,11 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
135139

136140
if (value != null) {
137141
if (!delegated) {
138-
element.addEventListener(event_name, value, opts);
142+
if (!prev) {
143+
events.push(() => element.addEventListener(event_name, value, opts));
144+
} else {
145+
element.addEventListener(event_name, value, opts);
146+
}
139147
} else {
140148
// @ts-ignore
141149
element[`__${event_name}`] = value;
@@ -177,6 +185,12 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
177185
}
178186
}
179187

188+
// On the first run, ensure that events are added after bindings so
189+
// that their listeners fire after the binding listeners
190+
if (!prev) {
191+
effect(() => events.forEach(run));
192+
}
193+
180194
return next;
181195
}
182196

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, target }) {
5+
const [i1, i2] = target.querySelectorAll('input');
6+
7+
i1?.click();
8+
await Promise.resolve();
9+
assert.htmlEqual(
10+
target.innerHTML,
11+
'true true <input type="checkbox"> false false <input type="checkbox">'
12+
);
13+
14+
i2?.click();
15+
await Promise.resolve();
16+
assert.htmlEqual(
17+
target.innerHTML,
18+
'true true <input type="checkbox"> true true <input type="checkbox">'
19+
);
20+
}
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
let checked_simple = $state(false);
3+
let checked_simple_copy = $state(false);
4+
5+
let checked_rest = $state(false);
6+
let checked_rest_copy = $state(false);
7+
let rest = $state(() => ({}));
8+
</script>
9+
10+
{checked_simple} {checked_simple_copy}
11+
<input type="checkbox" onchange={() => {checked_simple_copy = checked_simple}} bind:checked={checked_simple} />
12+
13+
{checked_rest} {checked_rest_copy}
14+
<!-- {...rest()} in order to force an isolated render effect -->
15+
<input type="checkbox" onchange={() => {checked_rest_copy = checked_rest}} {...rest()} bind:checked={checked_rest} />

0 commit comments

Comments
 (0)