diff --git a/.changeset/two-brooms-fail.md b/.changeset/two-brooms-fail.md new file mode 100644 index 000000000000..63ac672fc9cd --- /dev/null +++ b/.changeset/two-brooms-fail.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure spread events are added even when rerunning spread immediately diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 515d952bd463..d7ad037eb9d3 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -4,8 +4,7 @@ import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils 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'; +import { effect, effect_root } from '../../reactivity/effects.js'; import * as w from '../../warnings.js'; /** @@ -112,7 +111,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha // @ts-expect-error var attributes = /** @type {Record} **/ (element.__attributes ??= {}); - /** @type {Array<() => void>} */ + /** @type {Array<[string, any, () => void]>} */ var events = []; for (key in next) { @@ -145,7 +144,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha if (value != null) { if (!delegated) { if (!prev) { - events.push(() => element.addEventListener(event_name, value, opts)); + events.push([key, value, () => element.addEventListener(event_name, value, opts)]); } else { element.addEventListener(event_name, value, opts); } @@ -193,7 +192,22 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha // 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)); + // In edge cases it may happen that set_attributes is re-run before the + // effect is executed. In that case the render effect which initiates this + // re-run will destroy the inner effect and it will never run. But because + // next and prev may have the same keys, the event would not get added again + // and it would get lost. We prevent this by using a root effect. + const destroy_root = effect_root(() => { + effect(() => { + if (!element.isConnected) return; + for (const [key, value, evt] of events) { + if (next[key] === value) { + evt(); + } + } + destroy_root(); + }); + }); } return next; diff --git a/packages/svelte/tests/runtime-runes/samples/event-spread-rerun/_config.js b/packages/svelte/tests/runtime-runes/samples/event-spread-rerun/_config.js new file mode 100644 index 000000000000..2b320e0a862c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-spread-rerun/_config.js @@ -0,0 +1,15 @@ +import { test, ok } from '../../test'; + +export default test({ + mode: ['client'], + + async test({ assert, logs, target }) { + const input = target.querySelector('input'); + ok(input); + + input.value = 'foo'; + await input.dispatchEvent(new Event('input')); + + assert.deepEqual(logs, ['hi']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-spread-rerun/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-spread-rerun/main.svelte new file mode 100644 index 000000000000..7f480447e39d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-spread-rerun/main.svelte @@ -0,0 +1,7 @@ + + + console.log('hi')}> + +{!rest ? (rest = {}) : false}