Svelte is growing and has rapidly gain interest through the previous years as shown in the previous State of JS survey:
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?
- Our Svelte project
- Integrating Svelte Web Components into Angular
- 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:
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"
}
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>
If you want to try it out, Svelte has an online REPL
You should see something similar:
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';
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>
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:
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 -->
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
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));
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));
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"
}
We can now run npm run build
and see the 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
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 {}
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
:
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"
]
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 {}
... or can we?
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 {}
You can now see the Svelte Web Component running inside our Angular app!
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!
Top comments (0)