plugin-custom-elements
is a vite plugin designed to simplify building websites with custom elements.
Philosophy: Use the platform and build websites the old way.
The robust set if tools we have today are great and have their place, but sometimes you just need the basics and a little sugar. This plugin aims to minimally augment the tools provided by the platform and make them just a little easier to use.
This plugin is in its infancy and is bound to have issues.
Pre-1.0, patch changes should be safe, but minor version bumps may introduce breaking changes.
This is a project I use and intend to continue using, it will evolve an change in bursts, but I intend to keep it inline with the general philosophical principles.
Try it out
git clone https://github.com/WillsonSmith/vite-plugin-custom-elements.git
cd vite-plugin-custom-elements
npm i
npm run dev
What a project looks like:
Also see /src
src/
├─ components/
│ ├─ my-component.html
│ ├─ my-counter/
│ │ ├─ my-counter.html
│ │ ├─ my-counter.ts
│ ├─ highlight-text.ts
├─ index.html
vite.config.ts
// vite.config.ts
import { pluginCustomElement } from 'plugin-custom-elements';
import { defineConfig } from 'vite';
export default defineConfig({
root: 'src',
build: {
rollupOptions: {
input: 'src/index.html',
},
},
plugins: [
pluginCustomElement({
root: './src',
elementDir: 'components',
}),
],
});
HTML-based componentst live in a directory specified by elementDir (defaut: componenst)
in the Vite plugin's configuraiton. The markup within these components will replace any usage of those components in your HTML inpust. They use <slot>
elements to determine how to handle child elements wherever you use them.
<!-- components/my-component.html -->
<style>
.header {
margin: 0;
}
</style>
<h1 class="header"><slot></slot></h1>
index.html
pre-build
<!doctype html>
<html>
<head></head>
<body>
<my-component>Some text</my-component>
</html>
index.html
post-build
<!doctype html>
<html>
<head>
<style>
my-component .header {
margin: 0;
}
</style>
</head>
<body>
<my-component>
<h1 class="header">Some text</h1>
</my-component>
</html>
JavaScript components require no special directory definition. Instead, the plugin searches your root directory for any custom element definitions and knows where they live.
/*
* @element my-counter
*/
export class MyCounter extends HTMLElement {}
customElements.get('my-counter')
? undefined
: customElements.define('my-counter', MyCounter);
Notice the use of the JSDoc @element
, this is how the plugin determines what is or is not a custom element.
In line with the Philosophy of this project: the default behaviour of the plugin is to do nothing with this custom element. You have a few options to use it.
- import it in your index
<script type="module" src="./my-counter.ts"></module>
- add a
hydrate
attriubte to your use of the component. This will auto-import the script for you.
<my-conter hydrate></my-counter>
When you need markup associated with your JavaScript components you can include both an HTML-based component and a JavaScript component.
<!-- my-counter.html -->
<style>
.my-counter {
display: flex;
gap: 1rem;
}
</style>
<div class="my-counter">
<div class="count">0</div>
<button>Add</button>
</div>
/*
* @element my-counter
*/
export class MyCounter extends HTMLElement {
connectedCallback() {
const button = this.querySelector('button');
const count = this.querySelector('.count');
button.addEventListener('click', () => {
const curr = Number(count.textContent);
count.innerHTML = curr + 1;
});
}
}
customElements.get('my-counter')
? undefined
: customElements.define('my-counter', MyCounter);
When you use <my-counter></my-counter>
it will be replaced like an HTML-based component. You can then add your functionality by adding a hydrate
attribute: <my-counter hydrate></my-counter>
or including it as a script. You can also include the script within your HTML component.
<!-- my-counter.html -->
<style>
.my-counter {
display: flex;
gap: 1rem;
}
</style>
<div class="my-counter">
<div class="count">0</div>
<button>Add</button>
</div>
<script type="module" src="./my-counter.ts"></script>
The web platform has recently added support for Declarative Shadow DOM, you can take advantage of this in your components by wrapping any component content with a <template shadowrootmode="open"></template>
<!-- my-counter.html -->
<template shadowrootmode="open">
<style>
.my-counter {
display: flex;
gap: 1rem;
}
</style>
<div class="my-counter">
<div class="count">0</div>
<button>Add</button>
</div>
</template>
Note I have excluded the <script>
tag. As of now, Vite's default HTML building plugin does not traverse <template>
tags and so it will not transform any linked files within them. This also applies to any <link>
tags.
Shadow roots encapuslate styles so <style>
tags within a shadow root template will not scope style to a component tag name.
Components can exist within each other. A component's styles will be injected as a <style>
tag within a shadow root.