Rendering Svelte Components as Web Components in Angular: A Step by Step Guide
Tags: angular, svelte, webdev, tutorial
Published: 2023-07-24 05:30:00

[Svelte](https://svelte.dev) is growing and [has rapidly gain interest](https://2022.stateofjs.com/en-US/libraries/front-end-frameworks#front_end_frameworks_experience_linechart) through the previous years as shown in the previous State of JS survey:

![State of JS 2022 Svelte focus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kje2gglfd2kse0hdgqwu.png)

With its [comprehensive tutorial](https://svelte.dev/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](https://svelte.dev/tutorial/basics), 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: {% embed https://dev.to/this-is-learning/whats-new-in-svelte-4-performance-boosts-and-streamlined-development-42dc %}

## Table of Contents

- [What are Web Components?](#what-are-web-components)
- [Our Svelte project](#our-svelte-project)
  - [Setting up a new Project](#setting-up-a-new-project)
  - [Creating a Custom Svelte Component](#creating-a-custom-svelte-component)
  - [Transforming Svelte Component into a Web Component](#transforming-svelte-component-into-a-web-component)
  - [Compiling Svelte Component to Pure JavaScript](#compiling-svelte-component-to-pure-javascript)
- [Integrating Svelte Web Components into Angular](#integrating-svelte-web-components-into-angular)
  - [Setting up an Angular Application](#setting-up-an-angular-application)
  - [Consuming Svelte Web Components in Angular](#consuming-svelte-web-components-in-angular)
- [Takeaways](#takeaways)

---

## 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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components):

> 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](https://kit.svelte.dev).

### 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:

```json
{
  "name": "svelte-web-components",
  "version": "1.0.0",
  "scripts": { },
  "devDependencies": {
    "svelte": "^3.59.1"
  },
  "type": "module"
}
```

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:

```html
<!-- 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>
```

> If you want to try it out, Svelte has an [online REPL](https://svelte.dev/repl/hello-world?version=3.59.1)

You should see something similar:

![Counter unstyled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fg6dsjrm9xw8seviqibk.gif)

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

```js
// components/index.js
export { default as Counter } from './Counter.svelte';
```

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:

```html
<!-- 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>
```

> Since we will be using our component elsewhere, let's also style it a little so that it will be more pleasant to use
> {% details ✨ Additional CSS %}

```html
<!-- 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>
```

> {% enddetails %}

Our component is still running fine using its store:

![Styled Counter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gyjff0cli9y1301z1xt4.gif)

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](https://svelte.dev/docs#template-syntax-svelte-options) to specify which tag to use:

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

<!-- Counter component code here -->
```

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](https://esbuild.github.io/) to bundle or component. We will need to grab two more dependencies: [`esbuild`](https://www.npmjs.com/package/esbuild) and [`esbuild-svelte`](https://www.npmjs.com/package/esbuild-svelte):

```bash
npm i -D esbuild esbuild-svelte
```

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

```js
// 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));
```

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

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

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

```diff
{
  "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"
}
```

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

![ESBuild Output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pnu50728ec0ocn5ev9u4.png)

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:

```bash
ng new angular-wrapper --standalone --defaults
```

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

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

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/70b5zt6ysgmhu12kzrj6.png)

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

```diff
"styles": [
  "src/styles.css"
],
"scripts": [
+ "src/scripts/web-components.js"
]
```

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

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

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

... or can we?

![Angular custom elements schema error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3l3a1t293clqlljnwml7.png)

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.

```diff
+ 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 {}
```

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

![Svelte Counter in Angular](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ilkrebwrylgcsbm1vig.gif)

## 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](https://github.com/pBouillon/DEV.RenderingSvelteComponentInAngular)

---

I hope that you learn something useful there!

{% embed https://dev.to/pbouillon %}

---

> Photo by <a href="https://unsplash.com/@didsss?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Didssph</a> on <a href="https://unsplash.com/photos/PB80D_B4g7c?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>