diff --git a/.changeset/thick-yaks-eat.md b/.changeset/thick-yaks-eat.md new file mode 100644 index 000000000000..324cd3abf612 --- /dev/null +++ b/.changeset/thick-yaks-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: replay function invocations on custom element diff --git a/packages/svelte/src/compiler/compile/render_dom/index.js b/packages/svelte/src/compiler/compile/render_dom/index.js index 9a11013fc90a..c8532624de65 100644 --- a/packages/svelte/src/compiler/compile/render_dom/index.js +++ b/packages/svelte/src/compiler/compile/render_dom/index.js @@ -584,7 +584,11 @@ export default function dom(component, options) { const slots_str = [...component.slots.keys()].map((key) => `"${key}"`).join(','); const accessors_str = accessors .filter((accessor) => !writable_props.some((prop) => prop.export_name === accessor.key.name)) - .map((accessor) => `"${accessor.key.name}"`) + .map((accessor) => { + return `{ name: "${accessor.key.name}", can_proxy: ${ + accessor.value?.type === 'FunctionExpression' + } }`; + }) .join(','); const use_shadow_dom = component.component_options.customElement?.shadow !== 'none' ? 'true' : 'false'; diff --git a/packages/svelte/src/runtime/internal/Component.js b/packages/svelte/src/runtime/internal/Component.js index 4733b1d74594..fb9278cf1c4f 100644 --- a/packages/svelte/src/runtime/internal/Component.js +++ b/packages/svelte/src/runtime/internal/Component.js @@ -169,6 +169,8 @@ if (typeof HTMLElement === 'function') { $$cn = false; /** Component props data */ $$d = {}; + /** Component binding invocations recorded before the component was mounted, to playback on creation */ + $$b = []; /** `true` if currently in the process of reflecting component props back to attributes */ $$r = false; /** @type {Record} Props definition (name, reflected, type etc) */ @@ -270,6 +272,11 @@ if (typeof HTMLElement === 'function') { } }); + // Replay binding invocations + for (const binding of this.$$b) { + binding(); + } + // Reflect component props as attributes const reflect_attributes = () => { this.$$r = true; @@ -381,7 +388,7 @@ function get_custom_element_value(prop, value, props_definition, transform) { * @param {import('./public.js').ComponentType} Component A Svelte component constructor * @param {Record} props_definition The props to observe * @param {string[]} slots The slots to create - * @param {string[]} accessors Other accessors besides the ones for props the component has + * @param {Array<{name: string, can_proxy: boolean}>} accessors Other accessors besides the ones for props the component has * @param {boolean} use_shadow_dom Whether to use shadow DOM */ export function create_custom_element( @@ -415,9 +422,25 @@ export function create_custom_element( }); }); accessors.forEach((accessor) => { - Object.defineProperty(Class.prototype, accessor, { + Object.defineProperty(Class.prototype, accessor.name, { get() { - return this.$$c?.[accessor]; + if (this.$$c) { + return this.$$c[accessor.name]; + } else { + // This is only an approximation of what's possible. + // It only handles the case where the accessor is a function without a return value + if (accessor.can_proxy) { + return (...args) => { + this.$$b.push(() => { + this.$$c[accessor.name](...args); + }); + }; + } else { + throw new Error( + `Cannot call '${accessor.name}' before the component is connected to the DOM` + ); + } + } } }); }); diff --git a/packages/svelte/test/runtime-browser/custom-elements-samples/bindings/main.svelte b/packages/svelte/test/runtime-browser/custom-elements-samples/bindings/main.svelte new file mode 100644 index 000000000000..f1b8366e7982 --- /dev/null +++ b/packages/svelte/test/runtime-browser/custom-elements-samples/bindings/main.svelte @@ -0,0 +1,13 @@ + + + + +

{prop} {innerVar}

diff --git a/packages/svelte/test/runtime-browser/custom-elements-samples/bindings/test.js b/packages/svelte/test/runtime-browser/custom-elements-samples/bindings/test.js new file mode 100644 index 000000000000..1a84e74d5830 --- /dev/null +++ b/packages/svelte/test/runtime-browser/custom-elements-samples/bindings/test.js @@ -0,0 +1,18 @@ +import * as assert from 'assert.js'; +import { tick } from 'svelte'; +import './main.svelte'; + +export default async function (target) { + const element = document.createElement('my-app'); + element.prop = true; + element.toggle(); + target.appendChild(element); + await tick(); + const el = target.querySelector('my-app'); + + await tick(); + + assert.ok(!el.prop); + const p = el.shadowRoot.querySelector('p'); + assert.equal(p.textContent, 'false true'); +} diff --git a/packages/svelte/test/runtime-browser/custom-elements-samples/oncreate/main.svelte b/packages/svelte/test/runtime-browser/custom-elements-samples/oncreate/main.svelte index f31603606989..0b712c64f4e8 100644 --- a/packages/svelte/test/runtime-browser/custom-elements-samples/oncreate/main.svelte +++ b/packages/svelte/test/runtime-browser/custom-elements-samples/oncreate/main.svelte @@ -1,7 +1,7 @@