Svelte app wrapped in a web component
Tags: svelte, javascript, typescript, webdev
Published: 2021-02-16 23:21:48

## Ressources :pray:
Before starting I want to acknowledge [redradix](https://github.com/redradix) and [Andres Martin](https://github.com/MonkeyAndres), who made the hard work for me in this template [https://github.com/redradix/svelte-custom-element-template](https://github.com/redradix/svelte-custom-element-template)...

If you are in a hurry, you can directly go to have a look at the code here and play with it: [https://github.com/stefanonepa/svelte-component-ts](https://github.com/stefanonepa/svelte-component-ts)

## Why? :thinking:
As explained in the github repo redradix/svelte-custom-element-template:

>
>Building custom elements with Svelte is really easy but have a lot of limitations, in this template I'm trying to show the way I solve most of these limitations.

>Svelte current limitations:
>-   [Support nested custom elements](https://github.com/sveltejs/svelte/issues/3520)
>-   [Nested child components lose their css when the parent is used as a custom element](https://github.com/sveltejs/svelte/issues/4274)
>-   [Transitions in custom Elements](https://github.com/sveltejs/svelte/issues/1825)
>-   [Context API doesn't work for custom elements](https://github.com/sveltejs/svelte/issues/3422)

They solved a very simple use case which was how to wrap a svelte app inside a web component.

## How? :eyes:
How can we achieve this miracle (hacks inside):
1. Build the entry component as a web component
2. Build the sub component as svelte app
3. Inject the css of the sub components in the shadowRoot element
4. If transition are used replace the injection in the document into the shadow element

### 1. Build the shadowRoot wrapper web component
```js
// rollup.config.js
svelte({
  preprocess: sveltePreprocess({ sourceMap: !production }),
  compilerOptions: {
    dev: !production,
    customElement: true,
  },
  emitCss: false,
  include: './src/ShadowRoot.svelte',
}),
```

### 2. Build svelte to be injected in the web component wrapper
```js
// rolup.config.js
svelte({
  preprocess: sveltePreprocess({ sourceMap: !production }),
  compilerOptions: {
    dev: !production,
  },
  emitCss: true,
  exclude: './src/ShadowRoot.svelte',
}),
```

### 3. inject the generated css into the shadowRoot node
To catch the generated css I modified `rollup-plugin-css-only` locally to push the generated css on each changes (rebuild)
```js
// ./.rollup/css-only.js
...
generateBundle: function generateBundle(opts, bundle) {
  // Combine all stylesheets, respecting import order
  var css = '';
  for (var x = 0; x < order.length; x++) {
     var id = order[x];
     css += styles[id] || '';
  }

   // Emit styles through callback
   if (typeof options.output === 'function') {
     options.output(css, styles, bundle);
     return;
}
...
```

Then inject the css right into the bundle (:scream: Hack alert!) with one important caveat which is that the wrapper web component has to have a style set :boom:.

```js
import css from './.rollup/css-only'; 
// rollup.config.js
css({
  output(styles, styleNodes, bundle) {
    const match = production
     ? `.shadowRoot.innerHTML="`
     : `.shadowRoot.innerHTML = "`;

      const currentBundle = bundle[bundleFile];
      currentBundle.code = currentBundle.code.replace(
        match, `${match}<style>${styles}</style>`);
  },
}),
```

### 4. Include svelte transition if used into the shadow dom

Svelte gives use some very nice utilities like transition (cf. [https://svelte.dev/tutorial/transition](https://svelte.dev/tutorial/transition))

For my actual understanding is that svelte will inject dynamically computed styles into the head/document, and this won't allow the transition to apply into the shadow dom. That's why we need to replace the document injection by the shadow dom node.

```js
// rollup.config.js
replace({
  '.ownerDocument': '.getRootNode()',
  delimiters: ['', ''],
}),
replace({
  '.head.appendChild': '.appendChild',
  delimiters: ['', ''],
}),
```
## Result :checkered_flag:

We have a web component that wraps a svelte app and supports typescript and scss out of the box, with a DX (developer experience) that allows you to change the code and rebuild it automatically.

{% github stefanonepa/svelte-component-ts %}

## TODO :open_hands:

[ ] support hot reload

## Conclusion

I hope this will help everyone trying to create custom element using all the goodness provided by svelte. I would love to find something less hacky provided by the svelte contributors. But I am still very happy of the result.

Feel free to share your experiences with web components and svelte, ideas for improvement or just say hi :wave: