Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
VS-X opened this issue Jul 11, 2023 · 5 comments · Fixed by #8991
Closed

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

VS-X opened this issue Jul 11, 2023 · 5 comments · Fixed by #8991

Comments

@VS-X
Copy link

VS-X commented Jul 11, 2023

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

@dummdidumm
Copy link
Member

dummdidumm commented Jul 11, 2023

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>

@VS-X
Copy link
Author

VS-X commented Jul 11, 2023

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

@dummdidumm
Copy link
Member

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 added a commit that referenced this issue Jul 18, 2023
This should help everyone who has special needs and use cases around custom elements. Since Svelte components are wrapped and only run on connectedCallback, it makes sense to expose the custom element class for modification before that.
- fixes #8954 - use extend to attach the function manually and save possible values to a prop
- closes #8473 / closes #4168 - use extend to set the proper static attribute and then call attachInternals in the constructor
closes #8472 - use extend to attach anything custom you need
closes #3091 - pass `this` to a prop of your choice and use it inside your component
dummdidumm added a commit that referenced this issue Jul 18, 2023
This should help everyone who has special needs and use cases around custom elements. Since Svelte components are wrapped and only run on connectedCallback, it makes sense to expose the custom element class for modification before that.
- fixes #8954 / closes #8955 - use extend to attach the function manually and save possible values to a prop
- closes #8473 / closes #4168 - use extend to set the proper static attribute and then call attachInternals in the constructor
- closes #8472 - use extend to attach anything custom you need
- closes #3091 - pass `this` to a prop of your choice and use it inside your component
- add some doc for #8987
@dummdidumm
Copy link
Member

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>

...

@VS-X
Copy link
Author

VS-X commented Jul 20, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants