diff --git a/.changeset/sharp-shoes-look.md b/.changeset/sharp-shoes-look.md new file mode 100644 index 000000000000..2caf6d421b99 --- /dev/null +++ b/.changeset/sharp-shoes-look.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take into account registration state when setting custom element props diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 0276069eee49..8f6588e94aa1 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -201,7 +201,7 @@ export function set_xlink_attribute(dom, attribute, value) { } /** - * @param {any} node + * @param {HTMLElement} node * @param {string} prop * @param {any} value */ @@ -216,10 +216,21 @@ export function set_custom_element_data(node, prop, value) { set_active_reaction(null); set_active_effect(null); try { - if (get_setters(node).includes(prop)) { + if ( + // Don't compute setters for custom elements while they aren't registered yet, + // because during their upgrade/instantiation they might add more setters. + // Instead, fall back to a simple "an object, then set as property" heuristic. + setters_cache.has(node.nodeName) || customElements.get(node.tagName.toLowerCase()) + ? get_setters(node).includes(prop) + : value && typeof value === 'object' + ) { + // @ts-expect-error node[prop] = value; } else { - set_attribute(node, prop, value); + // We did getters etc checks already, stringify before passing to set_attribute + // to ensure it doesn't invoke the same logic again, and potentially populating + // the setters cache too early. + set_attribute(node, prop, value == null ? value : String(value)); } } finally { set_active_reaction(previous_reaction); @@ -384,8 +395,9 @@ function get_setters(element) { var setters = setters_cache.get(element.nodeName); if (setters) return setters; setters_cache.set(element.nodeName, (setters = [])); + var descriptors; - var proto = get_prototype_of(element); + var proto = element; // In the case of custom elements there might be setters on the instance var element_proto = Element.prototype; // Stop at Element, from there on there's only unnecessary setters we're not interested in diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/_config.js new file mode 100644 index 000000000000..0679c820ba8c --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../assert'; + +const tick = () => Promise.resolve(); + +// Check that rendering a custom element and setting a property before it is registered +// does not break the "when to set this as a property" logic +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + await tick(); + + const ce_root = target.querySelector('custom-element').shadowRoot; + + ce_root.querySelector('button')?.click(); + flushSync(); + await tick(); + await tick(); + + const inner_ce_root = ce_root.querySelectorAll('set-property-before-mounted'); + assert.htmlEqual(inner_ce_root[0].shadowRoot.innerHTML, 'object|{"foo":"bar"}'); + assert.htmlEqual(inner_ce_root[1].shadowRoot.innerHTML, 'object|{"foo":"bar"}'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/main.svelte new file mode 100644 index 000000000000..09a139e642b1 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + +{#if property} + +{/if}