sveltejs/svelte

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:

  1. Take arguments consistent with <svelte:options customElement={...} /> when called
  2. 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).
  3. 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.