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.
Top comments (3)
Heavy inspiration from Angular, I see.
It looks definitely nice, but there are some things that makes me wonder.
Those look like TypeScript decorators, and as such they're not ECMAScript. The specs are now on completely different paths. Maybe clarify that?
Using
innerText
instead oftextContent
or changing thedata
property of the text node is a peculiar choice.innerText
rearranges whitespaces under the hood and it's generally slower, but it can serve a purpose. Is this the case?There's another possible source of confusion here, as there's a stage 1 proposal called - you guessed - Emitter. It could be take a couple of years, sure, but still a possible name conflict.
All in all, it's a nice approach. There are several problems with Web Components, and you got to the point in targeting them. Boilerplate code, verbosity, non-intuitive interfaces... you name it. I think it's a step on the right direction.
Also, I like the choice of TypeScript.
innerText was a typo. It is textContent for sure π. Edited for clarity.
Ugh different spec paths for Decorators. I should read up on this more. From what I understand the stage 2 proposal is basically what TypeScript currently implements.
Didnβt know about Emitter, thatβs cool π.
Glad you like the direction. I definitely wanted to take some of the pain points away from developing Web Components.
Looks really sweet. I'll definitely give it a go this weekend.ππΌ