svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Custom Elements built with Svelte 4 do not work in Home Assistant

Open VS-X opened this issue 1 year ago • 3 comments

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.

  1. Create a new Vite + Svelte 4 project (npm init vite)
  2. Add a tag and the corresponding build option: <svelte:options customElement="svelte-component" />
  3. Add exported function setConfig to the component
  4. Run npm run build
  5. Add a built .js file as a new dashboard resource in Home Assistant
  6. 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

VS-X avatar Jul 11 '23 08:07 VS-X

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>

dummdidumm avatar Jul 11 '23 09:07 dummdidumm

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

VS-X avatar Jul 11 '23 11:07 VS-X

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.

dummdidumm avatar Jul 11 '23 11:07 dummdidumm

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>

...

dummdidumm avatar Jul 19 '23 15:07 dummdidumm

Excellent, thanks! I tested it with the aforementioned custom card for Home Assistant—it works as expected.

VS-X avatar Jul 20 '23 06:07 VS-X