.vscode | ||
assets | ||
examples | ||
site | ||
src | ||
.gitignore | ||
LICENSE | ||
package-lock.json | ||
package.json | ||
prepare.js | ||
README.md | ||
sample.html | ||
tsconfig.json |
Web Component Base
A zero-dependency, ~600 Bytes (minified & gzipped), JS base class for creating reactive custom elements easily
When you extend the WebComponent
class for your component, you only have to define the template
and properties
. Any change in any property value will automatically cause just the component UI to render.
The result is a reactive UI on property changes. View on CodePen ↗
Table of Contents
- Import via unpkg
- Installation via npm
- Usage
- `template` vs `render()`
- Prop access
- Quick Start Example
- Life-Cycle Hooks
- `onInit` - the component is connected to the DOM, before view is initialized
- `afterViewInit` - after the view is first initialized
- `onDestroy` - the component is disconnected from the DOM
- `onChanges` - every time an attribute value changes
- Library Size
Import via unpkg
Import using unpkg in your vanilla JS component. We will use this in the rest of our usage examples.
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
Installation via npm
Usable for projects with bundlers or using import maps.
npm i web-component-base
Usage
In your component class:
// HelloWorld.mjs
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
class HelloWorld extends WebComponent {
static properties = ["my-name", "emotion"];
get template() {
return `
<h1>Hello ${this.props.myName}${this.props.emotion === "sad" ? ". 😭" : "! 🙌"}</h1>`;
}
}
customElements.define('hello-world', HelloWorld);
In your HTML page:
<head>
<script type="module" src="HelloWorld.mjs"></script>
</head>
<body>
<hello-world my-name="Ayo" emotion="sad">
<script>
const helloWorld = document.querySelector('hello-world');
setTimeout(() => {
helloWorld.setAttribute('emotion', 'excited');
}, 2500)
</script>
</body>
The result is a reactive UI that updates on attribute changes:

template
vs render()
This mental model attempts to reduce the cognitive complexity of authoring components:
- The
template
is a read-only property (initialized with aget
keyword) that represents how the component view is rendered. - There is a
render()
method that triggers a view render. - This
render()
method is automatically called under the hood every time an attribute value changed. - You can optionally call this
render()
method at any point to trigger a render if you need (eg, if you have private unobserved properties that need to manually trigger a render) - Overriding the
render()
function for handling a customtemplate
--where you can returnstring | Node | (string | Node)[]
is also an option. More examples on this to follow.
Prop Access
The props
property of the WebComponent
interface is provided for easy read/write access to a camelCase counterpart of any observed attribute.
class HelloWorld extends WebComponent {
static properties = ["my-prop"];
onInit() {
let count = 0;
this.onclick = () => this.props.myProp = `${++count}`
}
get template() {
return `
<h1>Hello ${this.props.myProp}</h1>
`;
}
}
Assigning a value to the props.camelCase
counterpart of an observed attribute will trigger an "attribute change" hook.
For example, assigning a value like so:
this.props.myName = 'hello'
...is like calling the following:
this.setAttribute('my-name','hello');
Therefore, this will tell the browser that the UI needs a render if the attribute is one of the component's observed attributes we explicitly provided with static properties = ['my-name']
;
Note
The
props
property ofWebComponent
works likeHTMLElement.dataset
, exceptdataset
is only for attributes prefixed withdata-
. A camelCase counterpart usingprops
will give read/write access to any attribute, with or without thedata-
prefix. Another advantage overHTMLElement.dataset
is thatWebComponent.props
can hold primitive types 'number', 'boolean', 'object' and 'string'.
Alternatives
The current alternatives are using what HTMLElement
provides out-of-the-box, which are:
HTMLElement.dataset
for attributes prefixed withdata-*
. Read more about this on MDN.- Methods for reading/writing attribute values:
setAttribute(...)
andgetAttribute(...)
; note that managing the attribute names as strings can be difficult as the code grows.
Quick Start Example
Here is an example of using a custom element in a single .html file.
<!DOCTYPE html>
<html lang="en">
<head>
<title>WC Base Test</title>
<script type="module">
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
class HelloWorld extends WebComponent {
static properties = ["my-name"];
get template() {
return `<h1>Hello ${this.props.myName}!</h1>`;
}
}
customElements.define("hello-world", HelloWorld);
</script>
</head>
<body>
<hello-world my-name="Ayo"></hello-world>
<script>
const helloWorld = document.querySelector('hello-world');
setTimeout(() => {
helloWorld.props.myName = 'Ayo zzzZzzz';
}, 2500);
</script>
</body>
</html>
Life-Cycle Hooks
Define behavior when certain events in the component's life cycle is triggered by providing hook methods
onInit()
- Triggered when the component is connected to the DOM
- Best for setting up the component
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
class ClickableText extends WebComponent {
// gets called when the component is used in an HTML document
onInit() {
this.onclick = () => console.log(">>> click!");
}
get template() {
return `<span style="cursor:pointer">Click me!</span>`;
}
}
afterViewInit()
- Triggered after the view is first initialized
class ClickableText extends WebComponent {
// gets called when the component's innerHTML is first filled
afterViewInit() {
const footer = this.querySelector('footer');
// do stuff to footer after view is initialized
}
get template() {
return `<footer>Awesome site © 2023</footer>`;
}
}
onDestroy()
- Triggered when the component is disconnected from the DOM
- best for undoing any setup done in
onInit()
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
class ClickableText extends WebComponent {
clickCallback() {
console.log(">>> click!");
}
onInit() {
this.onclick = this.clickCallback;
}
onDestroy() {
console.log(">>> removing event listener");
this.removeEventListener("click", this.clickCallback);
}
get template() {
return `<span style="cursor:pointer">Click me!</span>`;
}
}
onChanges()
- Triggered when an attribute value changed
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
class ClickableText extends WebComponent {
// gets called when an attribute value changes
onChanges(changes) {
const {property, previousValue, currentValue} = changes;
console.log('>>> ', {property, previousValue, currentValue})
}
get template() {
return `<span style="cursor:pointer">Click me!</span>`;
}
}
Library Size
The bundle size was reported to be 587 Bytes (minified & gzipped) by bundlephobia. Running size-limit reports the base class size as around 760 Bytes (minified & brotlied), and using the WebComponent.min.js
version gets it down to around 400 Bytes.