Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sharp-shoes-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: take into account registration state when setting custom element props
21 changes: 16 additions & 5 deletions packages/svelte/src/internal/client/dom/elements/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function set_xlink_attribute(dom, attribute, value) {
}

/**
* @param {any} node
* @param {HTMLElement} node
* @param {string} prop
* @param {any} value
*/
Expand All @@ -161,10 +161,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);
Expand Down Expand Up @@ -324,10 +335,10 @@ var setters_cache = new Map();
/** @param {Element} element */
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = '<custom-element></custom-element>';
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"}');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<svelte:options customElement="custom-element" />

<script lang="ts">
import { onMount } from 'svelte';

class CustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
Object.defineProperty(this, 'property', {
set: (value) => {
this.shadowRoot.innerHTML = typeof value + '|' + JSON.stringify(value);
}
});
}
}

onMount(async () => {
customElements.define('set-property-before-mounted', CustomElement);
});

let property = $state();
</script>

<button onclick={() => (property = { foo: 'bar' })}>Update</button>
<!-- one that's there before it's registered -->
<set-property-before-mounted {property}></set-property-before-mounted>
<!-- and one that's after registration but sets property to an object right away -->
{#if property}
<set-property-before-mounted {property}></set-property-before-mounted>
{/if}
Loading