DEV Community

Cover image for Rendering Svelte Components as Web Components in Angular: A Step by Step Guide
Pierre Bouillon for This is Angular

Posted on

Rendering Svelte Components as Web Components in Angular: A Step by Step Guide

Svelte is growing and has rapidly gain interest through the previous years as shown in the previous State of JS survey:

State of JS 2022 Svelte focus

With its comprehensive tutorial, one might be tempted to take it a step further and to use Svelte in real app.

However, migrating a whole app Big Bang style might not be an easy way to dive into it. A solution might be to create separate Svelte component and consume them as Web Components.

When I first read the tutorial, I stopped on this sentence:

You can build your entire app with Svelte, or you can add it incrementally to an existing codebase. You can also ship components as standalone packages that work anywhere, without the overhead of a dependency on a conventional framework.

As a developer familiar with Angular, I wanted to see the process of creating Svelte components, compiling them to raw JavaScript and incorporating them as native elements into an existing Angular application to undergo this process.

In this article, we will explore how to integrate Svelte components into an existing Angular application as Web Components.

✋ Attention - Svelte 4 has just been released but this guide is using Svelte 3: the package used here are still using Svelte 3 as well so migrating now would be a bit premature.
If you would like to learn more about Svelte 4, I just blogged about it:

Table of Contents


What are Web Components?

Before diving head-first into the interoperability of Svelte and Angular, let's first see what will be the glue tying them together: Web Components.

As stated by the MDN web docs:

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.

At the core at this technology is the idea of exporting a piece of HTML with its logic, and being able to use it somewhere else without fearing any interference with the existing code.

Web Components have at their core three technologies:

  • 🧪 Custom Elements which is the ability to create our own HTML elements with their own behavior, style and/or templates
  • 🌑 Shadow DOM which act as a local DOM for the component, allowing it to define all of its elements in total isolation with the global DOM
  • 🧩 HTML Templates that provides a way to define and reuse chunk of HTML by inserting them into the DOM

In our context, it means that we would like to export a Svelte component (with its template, style and logic), and drop it in our Angular application.

Great! We now see a bit clearer where this is going. Let's jump to the code then!

Our Svelte Project

Before exporting any Svelte component, we will first need to create the component library.

To do so, we will scaffold a new library project, using SvelteKit.

Setting up a New Project

Since Svelte is "just" a compiler, we will create a raw JavaScript project.

In a new svelte-web-components folder, create a package.json file containing the following configuration:

{
  "name": "svelte-web-components",
  "version": "1.0.0",
  "scripts": { },
  "devDependencies": {
    "svelte": "^3.59.1"
  },
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

We're ready to go!

Creating a Custom Svelte Component

For our example, we will be exporting a counter that, given an initial value, can be incremented, decremented or reset.

Define the component's code in a new components/Counter.svelte file:

<!-- components/Counter.svelte -->
<script>
  export let initialValue = 0;

  let count = initialValue;
  $: isInitialValue = count === initialValue;

  const increment = () => (count += 1);
  const decrement = () => (count -= 1);
  const reset = () => (count = initialValue);
</script>

<div>
  <span>{count}</span>

  <button type="button" on:click={decrement}>-</button>
  <button type="button" on:click={increment}>+</button>
  <button type="button" on:click={reset} disabled={isInitialValue}>Reset</button>
</div>
Enter fullscreen mode Exit fullscreen mode

If you want to try it out, Svelte has an online REPL

You should see something similar:

Counter unstyled

Since we are building a library, let's not forget to expose it in our public API:

// components/index.js
export { default as Counter } from './Counter.svelte';
Enter fullscreen mode Exit fullscreen mode

In order to take advantage of Svelte capabilities, we will be using a store to manage the value of our counter.

This is absolutely not necessary here, but it will allows us to see if we can use such features when our component will be exported.

The updated version is only slightly different:

<!-- components/Counter.svelte -->
<script>
  import { writable } from 'svelte/store';

  export let initialValue = 0;

  let count = writable(initialValue);
  $: isInitialValue = $count === initialValue;

  const increment = () => count.update((n) => (n += 1));
  const decrement = () => count.update((n) => (n -= 1));
  const reset = () => count.set(initialValue);
</script>

<div>
  <span>{$count}</span>

  <button type="button" on:click={decrement}>-</button>
  <button type="button" on:click={increment}>+</button>
  <button type="button" on:click={reset} disabled={isInitialValue}>Reset</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Since we will be using our component elsewhere, let's also style it a little so that it will be more pleasant to use

✨ Additional CSS
<!-- components/Counter.svelte -->
<style>
  div {
    display: flex;
    align-items: center;
    gap: 5px;
    border: 1px solid #999;
    width: fit-content;
    padding: 5px;
    border-radius: 5px;
  }

  div span {
    font-size: 18px;
    font-weight: bold;
    margin: 0 10px;
  }

  div button {
    padding: 5px 10px;
    border: 1px solid #ccc;
    background-color: #f0f0f0;
    color: #333;
    font-size: 16px;
    transition: background-color 0.3s ease;
    border-radius: 5px;
  }

  div button:hover {
    background-color: #e0e0e0;
  }

  div button:active {
    background-color: #ccc;
  }

  div button:disabled {
    opacity: 40%;
  }
</style>

Our component is still running fine using its store:

Styled Counter

In fact, it is working so well that I may want to use it outside of this library, let's make that happen!

Transforming Svelte Component into a Web Component

As we previously saw, a way to run our component outside of this environment is to convert it to a Web Component.

Let's review the checklist and see what we have already checked:

  • 🧪 Custom Elements
  • ✅ 🌑 Shadow DOM
  • ✅ 🧩 HTML Templates

We're almost there!

To use our custom HTML element, we will need to define one for our component.

To do so, we can use the special <svelte:options> element to specify which tag to use:

<!-- components/Counter.svelte -->
<svelte:options tag="svelte-counter" />

<!-- Counter component code here -->
Enter fullscreen mode Exit fullscreen mode

We now have our Counter component satisfying all three requirements of a Web Component.

Compiling Svelte Component to Pure JavaScript

With our component ready to be exported, we can now do so by compiling it to pure JavaScript.

For this, we will leverage esbuild to bundle or component. We will need to grab two more dependencies: esbuild and esbuild-svelte:

npm i -D esbuild esbuild-svelte
Enter fullscreen mode Exit fullscreen mode

We can then add a script that will read the entrypoint of our library and output the JS result:

// esbuild-bundle.js
import esbuild from "esbuild";
import sveltePlugin from "esbuild-svelte";

esbuild
  .build({
    entryPoints: ["./components"],
    bundle: true,
    outfile: "dist/web-components.js",
    plugins: [
      sveltePlugin(),
    ],
    logLevel: "info",
  })
  .catch(() => process.exit(1));
Enter fullscreen mode Exit fullscreen mode

However, we should tell eslint that we are building web components, and not simply bundling our app:

esbuild
  .build({
    entryPoints: ["./components"],
    bundle: true,
    outfile: "dist/web-components.js",
    plugins: [
      sveltePlugin({
+       compilerOptions: {
+         customElement: true,
+       },
      }),
    ],
    logLevel: "info",
  })
  .catch(() => process.exit(1));
Enter fullscreen mode Exit fullscreen mode

Finally, to make it a bit easier to run, we can add an entry to the scripts of our package.json:

{
  "name": "svelte-web-components",
  "version": "1.0.0",
  "scripts": {
+   "build": "node esbuild-bundle.js"
  },
  "devDependencies": {
    "esbuild": "^0.18.2",
    "esbuild-svelte": "^0.7.3",
    "svelte": "^3.59.1"
  },
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

We can now run npm run build and see the output:

ESBuild Output

We should have our library output under dist/web-components.js containing our Web Component 📦

Integrating Svelte Web Components into Angular

With our bundle ready, all is left is to consume it.

Setting up an Angular Application

To bootstrap our Angular application, run the following command:

ng new angular-wrapper --standalone --defaults
Enter fullscreen mode Exit fullscreen mode

and replace the generated app.component.ts with the following code:

import { Component } from "@angular/core";

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Svelte in Angular!</h1>
  `,
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

Now that our Angular app is ready and our Svelte Web Component is too, let's wire this up!

Consuming Svelte Web Components in Angular

To make Angular aware of our external JS bundle, we will need to add it as a dependency of our project.

For that, grab the previously generated web-components.js file and drop it into a new folder at the root of your Angular project, under src/scripts:

Angular folder structure

And reference it in the angular.json file in the scripts array located at angular.json > projects > angular-wrapper > architect > build > options > scripts:

"styles": [
  "src/styles.css"
],
"scripts": [
+ "src/scripts/web-components.js"
]
Enter fullscreen mode Exit fullscreen mode

Angular having bundled our Web Component alongside with our app, we can now use our counter!

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Svelte in Angular!</h1>
+   <svelte-counter />
  `,
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

... or can we?

Angular custom elements schema error

In an attempt to prevent us from doing mistakes, Angular scans the HTML elements we are using and see if they are either a native HTML element or a resolvable Angular component.

However, in our case, this is neither.

Hopefully, Angular gives us a solution for our use case:

If 'svelte-counter' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.

+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";

@Component({
  selector: "app-root",
  standalone: true,
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <h1>Svelte in Angular!</h1>
    <svelte-counter />
  `,
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

You can now see the Svelte Web Component running inside our Angular app!

Svelte Counter in Angular

Takeaways

In this article, we saw what Web Components are, how to convert a Svelte component to a Web Component and how to consume it from an Angular application.

The process is pretty straightforward thanks to the community packages and the nature of Svelte itself: being a compiler, creating chunks of JS that can run anywhere is in its very own nature.

If you would like to check the resulting code, you can head on to the associated GitHub Repository


I hope that you learn something useful there!


Photo by Didssph on Unsplash

Top comments (0)