DEV Community

Cover image for Why I coded a micro library for Web Components
Stephen Belovarich
Stephen Belovarich

Posted on • Edited on

Why I coded a micro library for Web Components

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'});

Enter fullscreen mode Exit fullscreen mode
  • ButtonComponent is a predefined ES2015 class that extends HTMLButtonElement and links up some functions needed to support the template and style defined in the Component decorator and calls any methods added to the prototype of this class by other decorators. The interesting part here is ButtonComponent 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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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 uses document.createTreeWalker and Proxy to watch for changes and update attributes and textContent discretely.

  • Emitter defines an EventEmitter pattern that can use BroadcastChannel 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 up addEventListener 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)

Collapse
 
maxart2501 profile image
Massimo Artizzu • Edited

Heavy inspiration from Angular, I see.

It looks definitely nice, but there are some things that makes me wonder.

ECMAScript Decorators for Web Components

Those look like TypeScript decorators, and as such they're not ECMAScript. The specs are now on completely different paths. Maybe clarify that?

update attributes and innerText discretely.

Using innerText instead of textContent or changing the data 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?

Emitter defines an EventEmitter pattern that can use BroadcastChannel API

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.

Collapse
 
steveblue profile image
Stephen Belovarich • Edited

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.

Collapse
 
hyperpress profile image
John Teague • Edited

Looks really sweet. I'll definitely give it a go this weekend.๐Ÿ‘๐Ÿผ