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.
pull/11230/head
Simon Holthausen 2 years ago
parent de2d8a0bee
commit f73fb4be3e

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: make sure event attributes run after bindings

@ -4,6 +4,8 @@ import { get_descriptors, map_get, map_set, object_assign } from '../../utils.js
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
import { delegate } from './events.js';
import { autofocus } from './misc.js';
import { effect } from '../../reactivity/effects.js';
import { run } from '../../../shared/utils.js';
/**
* 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
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
/** @type {Array<() => void>} */
var events = [];
for (key in next) {
var value = next[key];
@ -135,7 +139,11 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
if (value != null) {
if (!delegated) {
element.addEventListener(event_name, value, opts);
if (!prev) {
events.push(() => element.addEventListener(event_name, value, opts));
} else {
element.addEventListener(event_name, value, opts);
}
} else {
// @ts-ignore
element[`__${event_name}`] = value;
@ -177,6 +185,12 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
}
}
// On the first run, ensure that events are added after bindings so
// that their listeners fire after the binding listeners
if (!prev) {
effect(() => events.forEach(run));
}
return next;
}

@ -0,0 +1,21 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [i1, i2] = target.querySelectorAll('input');
i1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
'true true <input type="checkbox"> false false <input type="checkbox">'
);
i2?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
'true true <input type="checkbox"> true true <input type="checkbox">'
);
}
});

@ -0,0 +1,15 @@
<script>
let checked_simple = $state(false);
let checked_simple_copy = $state(false);
let checked_rest = $state(false);
let checked_rest_copy = $state(false);
let rest = $state(() => ({}));
</script>
{checked_simple} {checked_simple_copy}
<input type="checkbox" onchange={() => {checked_simple_copy = checked_simple}} bind:checked={checked_simple} />
{checked_rest} {checked_rest_copy}
<!-- {...rest()} in order to force an isolated render effect -->
<input type="checkbox" onchange={() => {checked_rest_copy = checked_rest}} {...rest()} bind:checked={checked_rest} />
Loading…
Cancel
Save