svelte
svelte copied to clipboard
Custom Elements built with Svelte 4 do not work in Home Assistant
Describe the bug
Context: Home Assistant's entire frontend is composed of custom elements. All the built-in ones are written in Lit. One can create a custom dashboard card by adding a new resource, which consists of a JS file containing a custom element. For the custom element card to render, it has to export setConfig
function.
When a custom element is rendered, Home Assistant calls setConfig
on each such element in _createElement. Here's an extract:
const element = document.createElement(tag)
element.setConfig(config);
A custom element built with Svelte 3 works when added to Home Assistant, but Svelte 4's doesn't because of: TypeError: element.setConfig is not a function
.
Is the exported function not immediately available when the component is created?
Reproduction
Reproduction is a bit time-consuming, since you need to have a Home Assistant environment running.
- Create a new Vite + Svelte 4 project (
npm init vite
) - Add a tag and the corresponding build option:
<svelte:options customElement="svelte-component" />
- Add exported function
setConfig
to the component - Run
npm run build
- Add a built
.js
file as a new dashboard resource in Home Assistant - Add a new custom card to the dashboard
Here is an example component with the exported setConfig
:
<svelte:options customElement="svelte-card" />
<script>
export let hass;
let config = {}
export function setConfig (conf = {}) {
console.log('setConfig')
config = { ...conf }
}
</script>
<main>
test
</main>
And here is main.ts
:
// also tried export {default as default} from './App.svelte'
export * from './App.svelte'
It starts to work if you downgrade Svelte from 4 to 3 and re-build.
Logs
No response
System Info
System:
OS: macOS 13.4.1
CPU: (10) arm64 Apple M1 Pro
Memory: 39.09 MB / 32.00 GB
Shell: 3.6.1 - /opt/homebrew/bin/fish
Binaries:
Node: 18.13.0 - ~/.local/state/fnm_multishells/11850_1688733464692/bin/node
npm: 8.19.3 - ~/.local/state/fnm_multishells/11850_1688733464692/bin/npm
Browsers:
Safari: 16.5.1
npmPackages:
svelte: ^4.0.5 => 4.0.5
Severity
blocking an upgrade
In Svelte 4, the inner components are created after a tick when the custom element is mounted to the DOM. This should work:
const element = document.createElement(tag)
mountTarget.appendChild(element);
await Promise.resolve();
element.setConfig(config);
See #8457 for a more indepth explanation of the breaking change.
Looking at the code, it doesn't seem that you have control over this part of the code though. A workaround for you would be to manually append the function to the created custom element component and forward it to a prop which will be saved for later use. Something like this:
<svelte:options customElement="my-widget" />
<script context="module">
customElements.whenDefined('my-widget').then((element) => {
element.prototype.setConfig = function (config) {
this.config = config;
};
});
</script>
<script>
export let config;
</script>
Thanks! I finagled the first code snippet into the HA's frontend code just to confirm that it is the culprit. The custom element started to render correctly! But, as you said, I don't have the control over that code. I assume that this change would not be allowed in HA's repo, since custom elements generated by most other libraries work just fine.
The second snippet is getting executed (I see the added function on the element's prototype), but does not work for some reason, as I still get setConfig is not a function
Ah that's unfortunate, I guess whenDefined
will fire too late, when setConfig
within HA is already invoked (you could confirm by adding a console.log into the whenDefined
to see when it fires). #8955 or adjusting the timing behavior of custom elements for the first invokation is the only solution then, both which need adjustment in Svelte itself.
With the new extend
option you can now work around it like this:
<svelte:options
customElement={{
tag: 'custom-element',
extend: (customElementConstructor) => {
return class extends customElementConstructor {
// Add the function here, not below in the component so that
// it's always available, not just when the inner Svelte component
// is mounted
setConfig(config) {
this.config = config;
}
};
}
}}
/>
<script>
export let config;
</script>
...
Excellent, thanks! I tested it with the aforementioned custom card for Home Assistant—it works as expected.