From ef173e3a1ae8e08ad3eeef806be7b5d63c7af89e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 20:20:25 -0400 Subject: [PATCH] fix: stable attachments --- .../client/visitors/shared/element.js | 7 +-- .../client/dom/elements/attachments.js | 2 +- .../client/dom/elements/attributes.js | 44 +++++++++++++++++++ packages/svelte/src/internal/client/index.js | 1 + 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index a093a0bf4a96..f7d72cbb3b85 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -104,9 +104,10 @@ export function build_set_attributes( ); if (is_dynamic) { - context.state.init.push(b.let(attributes_id)); - const update = b.stmt(b.assignment('=', attributes_id, call)); - context.state.update.push(update); + context.state.init.push( + b.let(attributes_id), + b.stmt(b.call('$.set_attribute_effect', element_id, b.thunk(b.object(values)))) + ); } else { context.state.init.push(b.stmt(call)); } diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 6e3089a384c1..f3add0cf2e4a 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -5,7 +5,7 @@ import { effect } from '../../reactivity/effects.js'; * @param {() => (node: Element) => void} get_fn */ export function attach(node, get_fn) { - effect(() => { + return effect(() => { const fn = get_fn(); // we use `&&` rather than `?.` so that things like diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 3d1acbd31ce1..4c689e6969f7 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -18,6 +18,7 @@ import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; import { set_style } from './style.js'; import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; +import { block, branch, destroy_effect } from '../../reactivity/effects.js'; export const CLASS = Symbol('class'); export const STYLE = Symbol('style'); @@ -456,6 +457,49 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal return current; } +/** + * @param {Element & ElementCSSInlineStyle} element + * @param {() => Record} fn + * @param {string} [css_hash] + * @param {boolean} [skip_warning] + */ +export function set_attribute_effect(element, fn, css_hash, skip_warning = false) { + /** @type {Record} */ + var prev = {}; + + /** @type {Record} */ + var effects = {}; + + block(() => { + var next = fn(); + + for (const key in prev) { + if (!next[key]) { + element.removeAttribute(key); + } + } + + for (let symbol of Object.getOwnPropertySymbols(prev)) { + if (!next[symbol]) { + destroy_effect(effects[symbol]); + delete effects[symbol]; + } + } + + for (const key in next) { + set_attribute(element, key, next[key], skip_warning); + } + + for (let symbol of Object.getOwnPropertySymbols(next)) { + if (symbol.description === ATTACHMENT_KEY && next[symbol] !== prev[symbol]) { + effects[symbol] = branch(() => attach(element, () => next[symbol])); + } + } + + prev = next; + }); +} + /** * * @param {Element} element diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index b5f746b276bf..383e19522bf5 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -28,6 +28,7 @@ export { remove_input_defaults, set_attribute, set_attributes, + set_attribute_effect, set_custom_element_data, set_xlink_attribute, set_value,