Why I coded a micro library for Web Components
Stephen Belovarich
Posted on July 15, 2019
I know it seems like everyone is building micro this, micro that.
Micro services, micro frontends and now micro libraries?!
There are already excellent solutions out there for developing Web Components.
Some of the major JavaScript frameworks like Svelte and Angular even compile down to Custom Elements. This can be a little overkill though considering the amount of tooling that goes into compiling a modern JavaScript framework down to Web Components.
So why did I code another library?
Challenge myself
to build a framework that is modern, but has zero dependencies. I wanted a solution that uses only API found in the browser. This means some features require a polyfill, but that's OK. It turns out several APIs exist in the browser that allow you to build a micro library for UI that enables data binding, advanced event handling, animations and more!
- customElements
- createTreeWalker
- Proxy
- CustomEvent
- BroadcastChannel
- Web Animations
Taking the pain away
from developing Web Components is another goal of the project. There is a lot of boilerplate involved with coding custom elements that can be reduced. It can be difficult to switch between custom elements that allow ShadowDOM
and others that don't. Autonomous custom elements are treated differently than customized built-in elements. Event handling is only as good as typical DOM, requiring calls to addEventListener
and dispatchEvent
and even then you're stuck with how events typically bubble up. There's also the problem of updating a custom element's template, requiring selecting DOM and updating attributes and inner content. This opens up the opportunity for engineers to make not so performant choices. What if a library could just handle all of this?
Full control
is what I was after. If I want to change the way the library behaves, I can. Readymade can build it out to support SVG out of the box (it does), but it could also render GL objects if I wanted to support that. All that would need to happen is to swap out the state engine and boom, WebGL support. I experiment all the time with different UI and need something malleable.
Distribution
is a key aspect of another project I've been working on for quite some time. I wanted a way to distribute a library of UI components without any framework dependencies. The goal of that project is to provide a UI library < 20Kb. Readymade itself is ~3Kb with all the bells and whistles imported. Components built with Readymade can be used like any other DOM element in a project built with any JavaScript framework, provided the framework supports custom elements.
Decorators
are something I take for granted in Angular and I wanted to learn how these high order functions work. The micro library I built is highly dependent on this future spec, but that's OK too. Building the library from scratch with TypeScript also provides the additional benefits of type checking, IntelliSense, and gives me access to the excellent TypeScript compiler.
Enter Readymade
Readymade is a micro library for handling common tasks for developing Web Components. The API resembles Angular or Stencil, but the internals are different. Readymade uses the browser APIs listed above to give you a rich developer experience.
- 🎰 Declare metadata for CSS and HTML ShadowDOM template
- ☕️ Single interface for 'autonomous custom' and 'customized built-in' elements
- 🏋️ Weighing in ~1Kb for 'Hello World' (gzipped)
- 1️⃣ One-way data binding
- 🎤 Event Emitter pattern
- 🌲 Treeshakable
An example
The below example of a button demonstrates some of the strengths of Readymade.
import { ButtonComponent, Component, Emitter, Listen } from '@readymade/core';
@Component({
template:`
<span>{{buttonCopy}}</span>
`,
style:`
:host {
background: rgba(24, 24, 24, 1);
cursor: pointer;
color: white;
font-weight: 400;
}
`,
})
class MyButtonComponent extends ButtonComponent {
constructor() {
super();
}
@State()
getState() {
return {
buttonCopy: 'Click'
}
}
@Emitter('bang')
@Listen('click')
public onClick(event) {
this.emitter.broadcast('bang');
}
@Listen('keyup')
public onKeyUp(event) {
if (event.key === 'Enter') {
this.emitter.broadcast('bang');
}
}
}
customElements.define('my-button', MyButtonComponent, { extends: 'button'});
-
ButtonComponent
is a predefined ES2015 class that extendsHTMLButtonElement
and links up some functions needed to support thetemplate
andstyle
defined in theComponent
decorator and calls any methods added to the prototype of this class by other decorators. The interesting part here isButtonComponent
is composable. Below is a the definition.
export class ButtonComponent extends HTMLButtonElement {
public emitter: EventDispatcher;
public elementMeta: ElementMeta;
constructor() {
super();
attachDOM(this);
attachStyle(this);
if (this.bindEmitters) { this.bindEmitters(); }
if (this.bindListeners) { this.bindListeners(); }
if (this.onInit) { this.onInit(); }
}
public onInit?(): void;
public bindEmitters?(): void;
public bindListeners?(): void; public bindState?(): void;
public setState?(property: string, model: any): void;
public onDestroy?(): void;
}
State
allows you to define local state for an instance of your button and any properties defined in state can be bound to a template. Under the hood Readymade usesdocument.createTreeWalker
andProxy
to watch for changes and updateattributes
andtextContent
discretely.Emitter
defines an EventEmitter pattern that can useBroadcastChannel API
so events are no longer relegated to just bubbling up, they can even be emitted across browser contexts.Listen
is a decorator that wires upaddEventListener
for you, because who wants to type that all the time?
Readymade is now v1
so go and check it out on GitHub. The documentation portal is built with Readymade and available on Github Pages.
Posted on July 15, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.