DEV Community

Cover image for Svelte app wrapped in a web component
stefanonepa
stefanonepa

Posted on • Updated on

Svelte app wrapped in a web component

Ressources 🙏

Before starting I want to acknowledge redradix and Andres Martin, who made the hard work for me in this 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

Why? 🤔

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:

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

How? 👀

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

// rollup.config.js
svelte({
  preprocess: sveltePreprocess({ sourceMap: !production }),
  compilerOptions: {
    dev: !production,
    customElement: true,
  },
  emitCss: false,
  include: './src/ShadowRoot.svelte',
}),
Enter fullscreen mode Exit fullscreen mode

2. Build svelte to be injected in the web component wrapper

// rolup.config.js
svelte({
  preprocess: sveltePreprocess({ sourceMap: !production }),
  compilerOptions: {
    dev: !production,
  },
  emitCss: true,
  exclude: './src/ShadowRoot.svelte',
}),
Enter fullscreen mode Exit fullscreen mode

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)

// ./.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;
}
...
Enter fullscreen mode Exit fullscreen mode

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

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>`);
  },
}),
Enter fullscreen mode Exit fullscreen mode

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)

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.

// rollup.config.js
replace({
  '.ownerDocument': '.getRootNode()',
  delimiters: ['', ''],
}),
replace({
  '.head.appendChild': '.appendChild',
  delimiters: ['', ''],
}),
Enter fullscreen mode Exit fullscreen mode

Result 🏁

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.

Svelte-component-ts template 🎉

this template enables svelte to be used with a shadow DOM entry component and then sub component using the goodness of svelte.

This template has stealen inspiration (hacks) from https://github.com/redradix/svelte-custom-element-template thanks to https://github.com/MonkeyAndres

This template includes:

  • typescript support out of the box
  • sass support
  • babel with a minimal configuration (cf. rollup.config.js)

Recommended tools

Usage

Clone it with degit:

npx degit stefanonepa/svelte-component-ts my-new-component
cd my-new-component

yarn
yarn dev
Enter fullscreen mode Exit fullscreen mode

Constraints

  • setup a style in the entry element ShadowRoot.svelte.
  • ⚠️ Styles in the root component are not scoped by svelte, then choose carefully your selectors if you use some there ⚠️.

Why?

(from redradix/svelte-custom-element-template ☝️)

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

Svelte current limitations:





TODO 👐

[ ] 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 👋

Top comments (1)

Collapse
 
sirvizzy profile image
Martin • Edited

For future readers. Took me a while to figure out but this is now built into svelte. Please have a look at; github.com/sveltejs/svelte/issues/.... Just set the target element to the shadow root (don't forget to disable emitCss).