Better Svelte web-components

Fixing Svelte's customElement solution

Better Svelte web-components

Recently I was tasked with building a UI component that could be loaded into an application. I had heard about web components, but haven't had the chance to mess around with them. I also took the opportunity to look Svelte. The reason for the Svelte interest is that it doesn't download a framework to the browser, it compiles into useable plain JS. I want to avoid burdening browsers with loading my code as well as a large framework (like React) just for some small web component.

Why Web Components

I want to give site owners the option to customize the web component that will be installed on their site. Users can add "props" to a web component by simply adding them like you would a normal HTML element. Like so:

<MyWebComponent myprop="example" />

Why Svelte

Svelte is a great way to create applications that don't download a framework. This is especially useful when you are going to be writing just a small component instead of a large application. It is good to cut that overhead and provide snappy experiences.

This is not a tutorial on how to create a Svelte app. There are plenty of examples on how to do that. In this article, I used the degit method with rollup.

Why you might find this article useful

I ran into the issue of Svelte compiling all my files into individual web components. I only cared to have the root application turned into one. Svelte doesn't support this type of behavior out of the box very well, and following documentation to set <svelte:options tag=null /> doesn't work. You get Illegal constructor errors.

There is also an issue with importing svelte components that you have installed and using them in your app. I believe the reason is similar to the error when setting tag=null. Svelte tries to use the imported module as a web component instead of as a Svelte module. I could be off on the reasons why, but I know importing non-web component modules has problems.

I spent a lot of time on Github issues and in other locations to finally get my little web-component application working. Most of the credit goes to people helping out in Svelte's github issues. In particular Jawish and replace5.

Fix 1:

Only compile the root app, and not every component inside the application. This fixes importing files from your own project, as well as external ones.

The documentation says something like:

add customElement: true, to the rollup.config.js file:

    plugins: [
        svelte({
            customElement: true,

Instead, let's create 2 svelte plugins, instead of 1. Then, let's add a rule that we only compile to a web component files that end in .wc.svelte instead of .svelte. The other rule can be the standard rules except with an exception to ignore .wc.svelte files. To summarize, you may have had something like this generated from degit:

    plugins: [
        svelte({
            compilerOptions: {
                dev: !production
            }
        }),
        css({ output: 'bundle.css' }),
        resolve({
            browser: true,
            dedupe: ['svelte']
        }),
        ...
    ],

but now we have this:

    plugins: [
        svelte({
            preprocess,
            compilerOptions: {
                dev: !production, 
                customElement: false, 
            },
            exclude: /\.wc\.svelte$/ 
        }),
        svelte({
            preprocess,
            compilerOptions: {
                dev: !production,
                customElement: true,
            },
            include: /\.wc\.svelte$/
        }),
        ...
    ]

Great! Now we compile only one web component. With all .svelte files, we don't even have to include the janky tag=null anymore except in our root file. Just make sure that the App.svelte file now read App.wc.svelte and that it still includes the <svelte:options tag="my-component-name" />.

credit

Uh oh. Problem 2:

Styles don't work anymore! I believe this has to do with the shadow dom that gets created by the web-component. The svelte files getting imported try and attach to the regular dom even though the HTML using those styles are now hidden from those styles in the shadow dom. We have to make sure those styles get mounted inside our shadow dom.

We are going to solve this problem by grabbing our bundle.css, finding our shadow dom in the regular dom, and injecting those styles inside.

Inside the <script> tags in our root component, lets do this:

let element = document.querySelector("route-package-tracker");
  function setShadowStyle(host, styleContent) {
    var style = document.createElement("style");
    style.innerHTML = styleContent;
    host.shadowRoot.appendChild(style);
  }

  setShadowStyle(element, "@import './bundle.css'");

Woohoo! Styles are fixed!

credit

Cleanup

The last thing to do is you can now delete main.js and change your index.html

Just render your web component in the body with any props you want it to have:

<body>
  <my-web-component darkmode="true" />
</body>

and in rollup.config.js change input: src/main.js => input: src/App.wc.js

Here is a base project that you can clone to get started:

github.com/nardbagel/svelte-web-component-e..

One of my only gripes is not being able to pass boolean props like this: <web-component darkmode /> (darkmode would come through as true)

I thought I had that working at some point.

Now let's get container queries into the browser!