DEV Community

Cover image for How to Create a Web Component in Svelte
sil-vio
sil-vio

Posted on

How to Create a Web Component in Svelte

In this article we will see how to create a web components using the Svelte framework.
Before we start writing the code, let's first see what a web components is.

Intro to Web Components

Web components are a set of web platform APIs that allow you to create new custom, reusable and encapsulated HTML tags for use in web pages and web apps. Custom components and widgets are based on web component standards, work on modern browsers and they can be used with any HTML-compatible JavaScript library or framework.

Web components are based on four main specifications:

Custom Elements

Custom elements provide a way to build own fully-featured DOM elements. By defining a custom element, authors can inform the parser how to properly construct an element and how elements of that class should react to changes. Custom elements contain their own semantics, behaviors, markup and can be shared across frameworks and browsers.

Shadow DOM

The shadow DOM specification defines how to use encapsulated style and markup in web components. Being able to keep the markup structure, style, and behavior hidden and separate from other code on the page so that different parts do not clash.

ES Modules

The ES Modules specification defines the inclusion and reuse of JS documents in a standards based, modular, performant way. The JavaScript specification defines a syntax for modules, as well as some host-agnostic parts of their processing model. The specification defines the rest of their processing model: how the module system is bootstrapped, via the script element with type attribute set to "module", and how modules are fetched, resolved, and executed

HTML Template

The HTML template element specification defines how to declare fragments of markup that go unused at page load, but can be instantiated later on at runtime.

Web Components technology can be used independently or collectively.

How do I use a web component?

Using a web components is very simple. For example, it is possible to use the component present in the library of web components released from polymer, such as the following component:

https://www.webcomponents.org/element/@polymer/paper-button

Starting from a simple web page:

<!doctype html>
<html>
  <head>
    <title>This is the title of the webpage!</title>
  </head>
  <body>
      <h1>Test Page</h1>
      <p>This is an example paragraph.</p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

It's possible to import the script that contains the web components to start using the library component as if this were a simple html element.

<html>
  <head>
    <title>This is the title of the webpage!</title>
    <script type="module" src="https://npm-demos.appspot.com/@polymer/paper-button@3.0.1/paper-button.js?@polymer/paper-button@3.0.1"></script>
  </head>
  <body>
      <h1>Test Page</h1>
      <p>This is an example paragraph.</p>
      <paper-button raised class="indigo">button</paper-button>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

What is Svelte?

Svelte is a JavaScript framework written by Rich Harris. Svelte applications do not include framework references.
Whereas traditional frameworks like React, Vue or Angular do the bulk of their work in the browser, Svelte shifts that work into a compile step that happens when you build your app.
Svelte generates code to manipulate the DOM, which may give better client run-time performance.

Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.

The Svelte implementation of TodoMVC weighs 3.6kb zipped. For comparison, React plus ReactDOM without any app code weighs about 45kb zipped. It takes about 10x as long for the browser just to evaluate React as it does for Svelte to be up and running with an interactive TodoMVC.

  • Introducing Svelte: Frameworks without the framework

How to make a simple svelte web applications

To create a new svelte project we can start from the official template https://github.com/sveltejs/template.

To create a new project in the my-svelte-project directory, install its dependencies, and start a server you can type the following commands:

npx degit sveltejs/template my-svelte-project
cd my-svelte-project
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

By accessing the url http://localhost:5000 you will see the hello-world web app.

For this example we will create a clock component, you can copy the content of the file app.svelte from this link: https://svelte.dev/examples#clock.

Compiling to a custom elements (aka web components)

Svelte components can also be compiled to custom elements (aka web components) using the customElement: true compiler option. You should specify a tag name for the component using the svelte:options element.

<svelte:options tag="my-element">
Enter fullscreen mode Exit fullscreen mode

By default custom elements are compiled with accessors: true, which means that any props are exposed as properties of the DOM element. To prevent this, add accessors={false} to svelte:options.

To build to custom element we must:

  • add customElement: true, to the rollup.config.js file:
    plugins: [
        svelte({
            customElement: true,
Enter fullscreen mode Exit fullscreen mode
  • add in App.svelte
<svelte:options tag="svelte-clock">
Enter fullscreen mode Exit fullscreen mode

In case you not define this svelte:option the compiler will warning you with the following message

svelte plugin: No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/
Enter fullscreen mode Exit fullscreen mode
  • run "npm run build"

During development (npm run dev), live reloading will be enabled. This means that any changes made to your custom-element or the HTML will be immediately reflected in the browser.

Once the web components is ready we can run “npm run build” which will compile a minified, production-ready version of your custom element in the public/bundle.js file.
The compiler will take care of creating the Shadow DOM, applying attributes/properties, and defining your custom element.

To test the web components created we can utilize the http-server.
To install we can execute the following command:

npm install http-server -g
Enter fullscreen mode Exit fullscreen mode

Then we can create in the public directory the index.html, importing the bundle.js and declaring the custom element “svelte-clock”:

<!doctype html>
<html>
  <head>
    <title>This is the title of the webpage!</title>
    <script src="bundle.js"></script>
  </head>
  <body>
      <h1>Test Page</h1>
      <p>This is an example paragraph.</p>
      <svelte-clock/>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Executing the following command we can see the components in action:

> http-server -p 8080 -c-1 public/
Starting up http-server, serving public/
Available on:
  http://127.0.0.1:8080
Enter fullscreen mode Exit fullscreen mode

Svelte Web Components: Conclusion

Properties

Any props that your custom element accepts will automatically be transformed to element attributes at compile time. It is recommended to stick with lowercase attribute names as naming conventions like camelCase or PascalCase will not work in HTML.

To test we can add a simple properties to the custom element.

<script>
    ...
    export let clocktitle = "Svelte Clock"
    ...
</script>
...
<h1>{clocktitle}</h1>
...
Enter fullscreen mode Exit fullscreen mode

In our index.html we can now set the value

<svelte-clock clocktitle="My Clock"></svelte-clock>
Enter fullscreen mode Exit fullscreen mode

Events

Custom events emitted from within a Svelte 3 wrapped as a web-component do not bubble up to the web-component itself as normal DOM events (the custom event by default does not go past the boundaries of the shadowDom) and can not be handled in the usual manner within the template.

<svelte-clock custom-event="handler()">    
Enter fullscreen mode Exit fullscreen mode

Events are not emitted from components compiled to a custom element #3119

The native Svelte syntax for listening events on:mycustomevent doesn't works with events dispatched by a Svelte component exported to Custom Element.

May be related to this ? https://github.com/sveltejs/svelte/blob/a0e0f0125aa554b3f79b0980922744ee11857069/src/runtime/internal/Component.ts#L162-L171

Here is a reproduction repository :

https://github.com/vogloblinsky/svelte-3-wc-debug

svelte3-raw

Example using just Svelte syntax. Inner component dispatch a custom event 'message'. App component listen to it using on:message

It works !

//Inner.svelte
<script>
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher();

    function sayHello() {
        console.log('sayHello in child: ', 'Hello!');
        dispatch('message', {
            text: 'Hello!'
        });
    }
</script>

<button on:click={sayHello}>
    Click to say hello
</button>
//App.svelte
<script>
    import Inner from './Inner.svelte';

    function handleMessage(event) {
        console.log('handleMessage in parent: ', event.detail.text);
    }
</script>

<Inner on:message={handleMessage}/>

svelte3-wc

Example using just Svelte syntax and exporting component to Web Components. Inner component dispatch a custom event 'message'. App component listen to it using on:message

Same syntax doesn't work.

//Inner.svelte
<svelte:options tag="inner-btn"/>
<script>
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher();

    function sayHello() {
        console.log('sayHello in child: ', 'Hello!');
        dispatch('message', {
            text: 'Hello!'
        });
    }
</script>

<button on:click={sayHello}>
    Click to say hello
</button>
//App.svelte
<svelte:options tag="my-app" />
<script>
    import Inner from './Inner.svelte';

    function handleMessage(event) {
        console.log('handleMessage in parent: ', event.detail.text);
    }
</script>

<inner-btn on:message={handleMessage}/>

Vanilla JS works fine in public/index.html

const button = document
                    .querySelector('my-app')
                    .shadowRoot.querySelector('inner-btn');

                button.$on('message', e => {
                    console.log('handleMessage in page');
                });

To make it cross the boundaries of shadowDom we have to create a Custom Event as mentioned in the v2 docs for svelte. Custom events can be created in your Svelte component using the CustomEvent api. After defining a custom event, you can dispatch that event by calling this.dispatchEvent(event) in response to changes in your component.
Custom events cannot be dispatched in response to lifecycle methods. For instance, if you try to dispatch a custom event in your onMount lifecycle method, your event will not be dispatched.

To add a events we can add a button:

<button on:click="{dispatchSavedDateEvent}">Save Date</button>
Enter fullscreen mode Exit fullscreen mode

when is clicked we can emit a custom event:

function dispatchSavedDateEvent(e) {
   console.log("[dispatchSecondIsElapsedEvent] time: ", time);
   // 1. Create the custom event.
   const event = new CustomEvent("savedData", {
     detail: time,
     bubbles: true,
     cancelable: true,
     composed: true // makes the event jump shadow DOM boundary
   });

   // 2. Dispatch the custom event.
   this.dispatchEvent(event);
 }
Enter fullscreen mode Exit fullscreen mode

The read-only composed property of the Event interface returns a Boolean which indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM.

An alternative method is to utilize createEventDispatcher

import { createEventDispatcher } from 'svelte'; 
const dispatch = createEventDispatcher();
...
dispatch('second', {
       text: '10 seconds elapsed!'
     });
...
Enter fullscreen mode Exit fullscreen mode

In the index.html we must subscribe to the new event in the following way:

document.querySelector('svelte-clock')
    .$on('second', (e) => { console.log("[index.html][second]", e)})
Enter fullscreen mode Exit fullscreen mode

Imports

To import Svelte components we must declare each nested elements with the tag

<svelte:option tag="my-nested-element”>
Enter fullscreen mode Exit fullscreen mode

Declaring child components as custom elements, these elements are also available to the consumer.
The nested element use the same Shadow DOM as the parent, there is no way to set the Shadow DOM mode to "closed" for the nested element.

The main advantage in using Svelte.js for creating web components is that the final component has very small dimensions. In our small example the web component packaged in the bundle.js weighs only 7170 bytes, dimensions that if compared with web components created by other frameworks make our web components tens of times smaller and faster to be executed by the browser.

Top comments (6)

Collapse
 
vitorhugomattos profile image
Vitor Hugo

is there a way to disable shadow dom? i would like to have the component styled using the page style

Collapse
 
silvio profile image
sil-vio

Hi Vitor!
no at the moment it is not possible to disable shadow dom. There have been discussions about whether and how to enable this option in the future. You can read the opinion of Rich Harris (the creator of svelte) on the topic here.

Collapse
 
wfelippesilva profile image
William Felippe

Do you have any examples about tests? I have not see many informations about how to test web components with shadow dom

Collapse
 
bemisawa profile image
bemisawa

How do Svelte's slots work once items are compiled to web components? Is it possible to pass HTML in when using them on the page?

Collapse
 
silvio profile image
sil-vio

Hi bemisawa! yes it's possible to pass html to web components

For example if your web components is:

<div>
  <slot />
  <span>
    <slot name="secondslot"></slot>
  </span>
</div>
Enter fullscreen mode Exit fullscreen mode

you can use this way :

<my-component>
 default slot.
 <small slot="secondslot">second slot</small>
</my-componentn>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
juanfar profile image
Juan Fajardo

Hello, I'm looking for the way to svelte custom elements works in IE11, Someone can help me?