Skip to content

Commit a2539cf

Browse files
authored
fix: correctly set custom element props (#14508)
fixes #14391 In #13337 the "when to set as a property" logic for custom elements was adjusted. A bug was introduced during this, and it consists of several parts, of which the latter I'm not sure what's the best solution, hence opening this to discuss. The problem is that during set_custom_element_data, get_setters is the only check done to differentiate between setting the value as a prop (has a setter) or as an attribute (doesn't have a setter). The solution is to take into account whether or not the custom element is already registered, and defer getting (and caching) its setters until then. Instead, fall back to a "an object is always set as a prop" heuristic.
1 parent cb5734a commit a2539cf

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

.changeset/sharp-shoes-look.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: take into account registration state when setting custom element props

packages/svelte/src/internal/client/dom/elements/attributes.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export function set_xlink_attribute(dom, attribute, value) {
201201
}
202202

203203
/**
204-
* @param {any} node
204+
* @param {HTMLElement} node
205205
* @param {string} prop
206206
* @param {any} value
207207
*/
@@ -216,10 +216,21 @@ export function set_custom_element_data(node, prop, value) {
216216
set_active_reaction(null);
217217
set_active_effect(null);
218218
try {
219-
if (get_setters(node).includes(prop)) {
219+
if (
220+
// Don't compute setters for custom elements while they aren't registered yet,
221+
// because during their upgrade/instantiation they might add more setters.
222+
// Instead, fall back to a simple "an object, then set as property" heuristic.
223+
setters_cache.has(node.nodeName) || customElements.get(node.tagName.toLowerCase())
224+
? get_setters(node).includes(prop)
225+
: value && typeof value === 'object'
226+
) {
227+
// @ts-expect-error
220228
node[prop] = value;
221229
} else {
222-
set_attribute(node, prop, value);
230+
// We did getters etc checks already, stringify before passing to set_attribute
231+
// to ensure it doesn't invoke the same logic again, and potentially populating
232+
// the setters cache too early.
233+
set_attribute(node, prop, value == null ? value : String(value));
223234
}
224235
} finally {
225236
set_active_reaction(previous_reaction);
@@ -384,8 +395,9 @@ function get_setters(element) {
384395
var setters = setters_cache.get(element.nodeName);
385396
if (setters) return setters;
386397
setters_cache.set(element.nodeName, (setters = []));
398+
387399
var descriptors;
388-
var proto = get_prototype_of(element);
400+
var proto = element; // In the case of custom elements there might be setters on the instance
389401
var element_proto = Element.prototype;
390402

391403
// Stop at Element, from there on there's only unnecessary setters we're not interested in
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../assert';
3+
4+
const tick = () => Promise.resolve();
5+
6+
// Check that rendering a custom element and setting a property before it is registered
7+
// does not break the "when to set this as a property" logic
8+
export default test({
9+
async test({ assert, target }) {
10+
target.innerHTML = '<custom-element></custom-element>';
11+
await tick();
12+
await tick();
13+
14+
const ce_root = target.querySelector('custom-element').shadowRoot;
15+
16+
ce_root.querySelector('button')?.click();
17+
flushSync();
18+
await tick();
19+
await tick();
20+
21+
const inner_ce_root = ce_root.querySelectorAll('set-property-before-mounted');
22+
assert.htmlEqual(inner_ce_root[0].shadowRoot.innerHTML, 'object|{"foo":"bar"}');
23+
assert.htmlEqual(inner_ce_root[1].shadowRoot.innerHTML, 'object|{"foo":"bar"}');
24+
}
25+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<svelte:options customElement="custom-element" />
2+
3+
<script lang="ts">
4+
import { onMount } from 'svelte';
5+
6+
class CustomElement extends HTMLElement {
7+
constructor() {
8+
super();
9+
this.attachShadow({ mode: 'open' });
10+
Object.defineProperty(this, 'property', {
11+
set: (value) => {
12+
this.shadowRoot.innerHTML = typeof value + '|' + JSON.stringify(value);
13+
}
14+
});
15+
}
16+
}
17+
18+
onMount(async () => {
19+
customElements.define('set-property-before-mounted', CustomElement);
20+
});
21+
22+
let property = $state();
23+
</script>
24+
25+
<button onclick={() => (property = { foo: 'bar' })}>Update</button>
26+
<!-- one that's there before it's registered -->
27+
<set-property-before-mounted {property}></set-property-before-mounted>
28+
<!-- and one that's after registration but sets property to an object right away -->
29+
{#if property}
30+
<set-property-before-mounted {property}></set-property-before-mounted>
31+
{/if}

0 commit comments

Comments
 (0)