Using customElements.define() is not possible in v4
patricknelson opened this issue · 8 comments
Describe the bug
For v4, improvements were made in how custom elements were compiled in #8457:
Instead of compiling to a custom element class, the Svelte component class is mostly preserved as-is. Instead a wrapper is introduced which wraps a Svelte component constructor and returns a HTML element constructor
So, since components are no longer compiled directly to an instance of HTMLElement
, the traditional v3 approach of manually defining your custom element will no longer work:
import ExampleElement from './lib/ExampleElement.svelte';
customElements.define('example-element', ExampleElement);
Unfortunately, a public API doesn't exist for handling this. To workaround it right now, you need to reach into Svelte's internal API:
import ExampleElement from './lib/ExampleElement.svelte';
import { create_custom_element } from 'svelte/internal'; // ❌
customElements.define('example-element',
// ❌
create_custom_element(
ExampleElement, // Component constructor
{}, // props_definition
[], // slots
[], // accessors
false, // use_shadow_dom
)
);
Possible Solutions
In v4, we still need generate a class descending from HTMLElement
. Maybe we could get a new wrapper function that would either:
- Take arguments consistent with
<svelte:options customElement={...} />
when called - Just pass the component itself and Svelte can infer all the necessary options defined in
<svelte:options customElement={...} />
in the component itself for us (closer parity to v3). - Hybrid approach, i.e.: Take arguments but make them optional. If omitted, failover to inferring options defined in
<svelte:options customElement={...} />
. Explicitly passed options to this wrapper would probably take precedence over the ones inferred from the component file.
The goal of this would be to simplify documentation and refactoring as much as possible. That should hopefully make it far easier to reason about when reading docs or when needing to refactor/separate the options out when manually calling customElements.define()
.
For example:
main.js
import ExampleElement from './lib/ExampleElement.svelte';
import { createCustomElement } from 'svelte'; // ✔️
customElements.define('example-element',
// ✔️
createCustomElement(
ExampleElement,
// Options from <svelte:options customElement={...} /> (excluding "tag")
{
shadow: 'none',
props: {
greetPerson: { reflect: true, attribute: 'greet-person' },
},
}
)
);
ExampleElement.svelte
<svelte:options
customElement={{
tag: null,
shadow: 'none',
props: {
greetPerson: { reflect: true, attribute: 'greet-person' },
},
}}
/>
<script>
export let greetPerson = 'world';
</script>
<h1>Hello {greetPerson}!</h1>
Reproduction
Code: https://github.com/patricknelson/svelte-v4-custom-elements-define
Init repo
git clone https://github.com/patricknelson/svelte-v4-custom-elements-define.git
cd svelte-v4-custom-elements-define
npm i
Reproduce bug
git checkout main
npm run dev
Test workaround
git checkout workaround
npm run dev
Logs
No response
System Info
System:
OS: Linux 5.15 Debian GNU/Linux 11 (bullseye) 11 (bullseye)
CPU: (16) x64 Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
Memory: 2.41 GB / 5.79 GB
Container: Yes
Shell: 5.1.4 - /bin/bash
Binaries:
Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node
Yarn: 1.22.19 - ~/.nvm/versions/node/v16.14.2/bin/yarn
npm: 8.5.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
Browsers:
Chrome: 111.0.5563.146
Severity
blocking an upgrade
Assuming you compile with custom element mode, then customElements.define("tag-name", Component.element)
works.
We need to document this.
I just tested that @dummdidumm and unfortunately that doesn't seem to be a complete workaround since HMR doesn't seem to work with that at all. Is there something else I need to do to facilitate HMR in Vite? 🤔
// HMR not working
import ExampleElement from './lib/ExampleElement.svelte';
customElements.define('example-element', ExampleElement.element);
vs.
// HMR works
import ExampleElement from './lib/ExampleElement.svelte';
import { create_custom_element } from 'svelte/internal';
customElements.define('example-element',
create_custom_element(
ExampleElement,
{},
[],
[],
false,
)
);
Setup a repro of the HMR issue in the hmr-bug
branch at https://github.com/patricknelson/svelte-v4-custom-elements-define
git clone https://github.com/patricknelson/svelte-v4-custom-elements-define.git
cd svelte-v4-custom-elements-define
npm i
git checkout hmr-bug
npm run dev
oh, this is nice "side effect" of the changes to custom elements. hmr for these didn't work at all in svelte-3
you would have to ensure that customElements.define
is only called once, as redifining them won't work. But the wrapper inside doing hmr would be awesome. Not sure how callbacks and prop changes would work but as long as the external interface stays unchanged, you'd be good to go.
Not sure if svelte-hmr could do something special to improve this even more.
cc @rixo
Yeah, would definitely love to get HMR along with custom elements. My primary use case is in a fairly large and old codebase that cannot be converted to a Svelte (or SvelteKit) app.
That said, svelte-tag works flawlessly with combining both custom elements and HMR. It has some other issues though (mainly with slots and handling attributes with capital letters) so I figured it might be best to work toward improving Svelte itself as a longer term solution.
Putting this (i.e. HMR compatible declaration of Svelte v4 custom elements) on my roadmap to support in v2 of my new package svelte-retag
(v1 being backward compatible with svelte-tag
, as it is a fork). In v2 of the release, svelte-retag
's API could theoretically support Svelte 4's <svelte:options customElements={...} />
syntax to ensure HMR support, e.g.
// Totally hypothetical...
svelteRetag({
tag: 'example-element',
shadow: 'none',
props: {
greetPerson: { reflect: true, attribute: 'greet-person' },
},
});
All this is assuming of course HMR support isn't added by the time Svelte v4 comes out (since ideally svelte-retag
itself would become irrelevant when v4 is out 🤞). Might have to create a new ticket for that. But essentially, right now what I'm getting with my svelte-retag
package is basically the whole suite of features 🍰:
- Fully composable Svelte 3 components (use as
<ExampleElement/>
as well as<example-element>
) - Vite HMR support out of the box, so you get instant updates without issue since we're just directly instantiating the component like normal (instead of referencing it like above)
- Supports bug-free nesting of custom elements (addressing issues in original
svelte-tag
repo, ✅ patricknelson/svelte-retag#5) - Support for early execution (i.e. before tag parsing) using IIFE, which can be an issue if component initializes before slots are even made available to it (uses
MutationObserver
) (✅ patricknelson/svelte-retag#7) - Lit-style attributes (WIP ⏳, see crisward/svelte-tag#16)
- Potential support for context (see crisward/svelte-tag#8)
Apologies for this comment getting slightly out of hand, but I'm so excited for Svelte 4's upcoming Custom Elements that I want to do it now but also not lose functionality if I can help it (particularly simple bug-free HMR)! 😅
It appears this is now documented at https://svelte.dev/docs/custom-elements-api:
import MyElement from './MyElement.svelte';
customElements.define('my-element', MyElement.element);
// In Svelte 3, do this instead:
// customElements.define('my-element', MyElement);
I'll close this out and instead reopen a new ticket to address the HMR support as a separate feature request.
@dominikg: Should that ticket go here or into svelte-hmr
?
svelte-hmr.