From f73fb4be3e3e357e4fb4882a917bbd9461b099d8 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 18 Apr 2024 22:53:54 +0200 Subject: [PATCH] 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. --- .changeset/seven-garlics-serve.md | 5 +++++ .../client/dom/elements/attributes.js | 16 +++++++++++++- .../event-attribute-after-binding/_config.js | 21 +++++++++++++++++++ .../event-attribute-after-binding/main.svelte | 15 +++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/seven-garlics-serve.md create mode 100644 packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/main.svelte diff --git a/.changeset/seven-garlics-serve.md b/.changeset/seven-garlics-serve.md new file mode 100644 index 0000000000..4c56f95b42 --- /dev/null +++ b/.changeset/seven-garlics-serve.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: make sure event attributes run after bindings diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 94ea12be99..d4551a248f 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -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} **/ (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; } diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/_config.js new file mode 100644 index 0000000000..e5ec3698e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/_config.js @@ -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 false false ' + ); + + i2?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + 'true true true true ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/main.svelte new file mode 100644 index 0000000000..5446c2a182 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-after-binding/main.svelte @@ -0,0 +1,15 @@ + + +{checked_simple} {checked_simple_copy} + {checked_simple_copy = checked_simple}} bind:checked={checked_simple} /> + +{checked_rest} {checked_rest_copy} + + {checked_rest_copy = checked_rest}} {...rest()} bind:checked={checked_rest} />