Web Components in Easy Mode https://webcomponent.io
Find a file
2023-11-18 23:27:23 +01:00
.vscode chore: add LICENSE file 2023-11-10 21:56:12 +01:00
assets chore: update examples 2023-09-16 23:08:52 +02:00
demo feat: more explainer for using WebComponent.props 2023-11-18 10:31:12 +01:00
src refactor: add return types; organize code 2023-11-18 22:47:26 +01:00
.gitignore feat: publish type definitions 2023-09-18 22:52:38 +02:00
LICENSE chore: add LICENSE file 2023-11-10 21:56:12 +01:00
package-lock.json chore: add check:size script using size-limit 2023-11-18 22:34:31 +01:00
package.json chore: add check:size script using size-limit 2023-11-18 22:34:31 +01:00
README.md chore: add library size section 2023-11-18 23:27:01 +01:00
sample.html update sample demo page 2023-11-18 23:27:23 +01:00

Web Component Base

Package information: NPM version Package information: NPM license Package information: NPM downloads

This provides a zero-dependency, ~760 Bytes (minified & brotlied), 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.

Table of Contents

  1. Import via unpkg
  2. Installation via npm
  3. Usage
  4. template vs render()
  5. Prop access
    1. Alternatives
  6. Quick Start Example
  7. Life-Cycle Hooks
    1. onInit - the component is connected to the DOM, before view is initialized
    2. afterViewInit - after the view is first initialized
    3. onDestroy - the component is disconnected from the DOM
    4. oChanges - every time an attribute value changes
  8. 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/index.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/index.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');
        // or:
        helloWorld.props.emotion = 'excited';
      }, 2500)
  </script>
</body>

The result is a reactive UI that updates on attribute changes:

UI showing feeling toward Web Components changing from SAD to EXCITED

template vs render()

This mental model attempts to reduce the cognitive complexity of authoring components:

  1. The template is a read-only property (initialized with a get keyword) that represents how the component view is rendered.
  2. There is a render() method that triggers a view render.
  3. This render() method is automatically called under the hood every time an attribute value changed.
  4. You can optionally call this render() method at any point to trigger a render if you need.

Prop Access

The WebComponent.props read-only property is provided for easy access to any observed attribute.

This API gives us read/write access to any attribute properties:

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 value to the props.camelCase counterpart 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'];

This works like HTMLElement.dataset, except dataset is only for attributes prefixed with data-*. A camelCase counterpart using props will give read/write access to any attribute, with or without the data-* prefix.

Alternatives

The current alternatives are using what HTMLElement provides out-of-the-box, which are:

  1. HTMLElement.dataset for attributes prefixed with data-*
  2. Methods for reading/writing attribute values: setAttribute(...) and getAttribute(...); 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/index.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/index.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 &copy; 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/index.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/index.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

Running size-limit reports the library size as around 767 B, minified and brotlied. Read more about how to minify and compress payload with brotli for your app.