npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@default-js/defaultjs-html-components

v3.0.0

Published

toolbox to build async web components

Downloads

408

Readme

@default-js/defaultjs-html-components

A small toolbox to build asynchronous web components.

Every component exposes a ready promise that resolves once its asynchronous initialization has finished, so you can reliably wait for a component to be set up before interacting with it. The library is built on top of @default-js/defaultjs-extdom, which adds jQuery-like helpers (attr, find, trigger, parent, …) to DOM nodes.

  • ⚡ async lifecycle with a ready promise
  • 🧩 works with autonomous custom elements and customized built-in elements
  • 🎛️ optional Shadow DOM, auto-generated ids and post-construct hooks
  • 📣 automatic attribute-change / change events
  • 🧰 small utility belt (event names, DOM traversal, styles, per-node data)
  • 📘 .d.ts TypeScript definitions included

Installation

npm install @default-js/defaultjs-html-components

The package depends on @default-js/defaultjs-extdom, @default-js/defaultjs-common-utils and @default-js/defaultjs-expression-language; they are installed automatically. Importing the package also imports defaultjs-extdom as a side effect, so the extended node helpers are available everywhere.


Quick start

Create a component by extending Component, expose a static NODENAME (the tag name) and register it with define:

import { Component, define } from "@default-js/defaultjs-html-components";

class HelloWorld extends Component {
  static get NODENAME() { return "d-hello"; }

  constructor() {
    super({
      shadowRoot: true,
      content: () => `<p>Hello, <slot></slot>!</p>`,
    });
  }
}

define(HelloWorld);
<d-hello>World</d-hello>

NODENAME must be defined on every concrete component — the base class throws a descriptive error if you forget it.

You can also register through the static helper on the class itself:

HelloWorld.define(HelloWorld);

The ready lifecycle

When a component is connected to the document its init() method runs. Once init() completes, the ready promise resolves:

const el = document.querySelector("d-hello");

await el.ready;          // resolves after init() has finished
el.ready.resolved;       // true once settled

Override init() to run asynchronous setup. It runs automatically after the post-construct hooks once the element is connected, and this.root already points at the render root:

class UserCard extends Component {
  static get NODENAME() { return "d-user-card"; }

  constructor() {
    super({ shadowRoot: true });
  }

  async init() {
    const id = this.attr("user-id");
    const user = await fetch(`/api/users/${id}`).then((r) => r.json());
    this.root.append(`<h2>${user.name}</h2>`);
  }
}

define(UserCard);

init() is a pure override hook — do not call super.init(). The framework runs the post-construct hooks (including the createUID id assignment) before init(), so overriding it never drops any setup. Whatever init() returns is used to resolve ready. To re-run the whole initialization on an already-connected component, call await el.reinit().

const card = document.querySelector("d-user-card");
await card.ready;   // guaranteed the user data has been rendered

Note on errors: if init() throws (or rejects with) an Error, the ready promise is rejected with it, and ready.error becomes true. Any other thrown value resolves ready with that value instead.

try {
  await el.ready;
} catch (error) {
  // init() threw an Error
}

When the element is removed from the DOM, destroy() runs and resets ready, so re-inserting the element initializes it again.


Constructor options

Pass an options object to super(...):

| Option | Type | Default | Description | | ---------------- | ------------------------------------------------- | ------- | ----------------------------------------------------------------------- | | shadowRoot | boolean | false | Attach an open shadow root. this.root then points at the shadow root. | | content | Node \| string \| (component) => Node \| string | null | Content appended to the root. Strings are parsed as HTML. | | createUID | boolean | false | Assign a unique id attribute during initialization. | | uidPrefix | string | "id-" | Prefix for the generated id. | | uidSuffix | string | "" | Suffix for the generated id. | | postConstructs | Array<(component) => void \| Promise> | null | Extra functions run during init() (see below). | | postConstucts | Array<(component) => void \| Promise> | null | Deprecated misspelled alias of postConstructs. |

⚠️ postConstucts (missing the second r) is kept only for backwards compatibility and logs a deprecation warning. Use postConstructs in new code. If both are given, postConstructs wins.

Content

// string (parsed as HTML)
super({ content: `<button type="button">click</button>` });

// factory function receiving the component instance
super({ content: (self) => `<label for="${self.id}">${self.attr("label")}</label>` });

// or a real node
super({ content: document.createElement("hr") });

Auto-generated ids

class Field extends Component {
  static get NODENAME() { return "d-field"; }
  constructor() {
    super({ createUID: true, uidPrefix: "field-" });
  }
}
define(Field);
// -> <d-field id="field-…unique…">

Post-construct hooks

postConstructs are executed in order as part of init() — a convenient way to compose setup steps without overriding init():

super({
  postConstructs: [
    async (self) => { self.root.append(`<slot></slot>`); },
    async (self) => { await self.loadData(); },
  ],
});

The full list is available via the read-only postConstructs getter. The framework runs these hooks before init(), so overriding init() never disables them and no super.init() call is required.


Reacting to attribute changes

Declare the attributes you care about via observedAttributes. When one of them changes, the component dispatches two events on itself:

  • <nodename>--attribute--<attribute> — a specific attribute changed
  • <nodename>--change — something on the component changed
class Toggle extends Component {
  static get NODENAME() { return "d-toggle"; }
  static get observedAttributes() { return ["checked"]; }
}
define(Toggle);
const toggle = document.querySelector("d-toggle");

toggle.addEventListener("d-toggle--attribute--checked", () => {
  console.log("checked changed to", toggle.attr("checked"));
});

toggle.addEventListener("d-toggle--change", () => {
  console.log("toggle changed");
});

Instead of hard-coding the event names, build them with the EventHelper:

import { utils } from "@default-js/defaultjs-html-components";
const { componentEventname, attributeChangeEventname } = utils.EventHelper;

toggle.addEventListener(attributeChangeEventname("checked", toggle), handler);
toggle.addEventListener(componentEventname("change", toggle), handler);

The separator (-- by default) is configurable — see Settings.


Extending built-in elements

Use componentBaseOf to build a component on top of a specific HTML element type (a customized built-in element):

import { componentBaseOf, define } from "@default-js/defaultjs-html-components";

class FancyInput extends componentBaseOf(HTMLInputElement) {
  static get NODENAME() { return "d-fancy-input"; }

  async init() {
    this.classList.add("fancy");
  }
}

// customized built-in elements are registered with an `extends` option
define(FancyInput, { extends: "input" });
<input is="d-fancy-input" type="text" />

The resulting class keeps all members of the base element (value, checked, …) and adds the component members (ready, init, root, …). Classes are cached per base type, so componentBaseOf(HTMLInputElement) always returns the same class. The default Component export is simply componentBaseOf(HTMLElement).


Utilities

All helpers are grouped under the utils export.

import { utils } from "@default-js/defaultjs-html-components";

utils.EventHelper

Build the event names used by the library.

utils.EventHelper.componentEventname("change", "d-foo");
// -> "d-foo--change"

utils.EventHelper.attributeChangeEventname("disabled", "d-foo");
// -> "d-foo--attribute--disabled"

node may be a tag-name string, an HTMLElement, or any object with a string NODENAME. An optional third separator argument overrides the default.

utils.NodeHelper

Traverse the DOM relative to a node.

// nearest ancestor matching a predicate
const form = utils.NodeHelper.findParent(input, (n) => n.nodeName === "FORM");

// shallowest matching descendant
const label = utils.NodeHelper.findClosestInDepth(root, (n) => n.nodeName === "LABEL");

utils.StyleHelper

Copy styles between nodes or load a component stylesheet by convention.

// clone <style> / <link rel="stylesheet"> from the light DOM into the shadow root
utils.StyleHelper.copyStyles(this, this.root);

// append <link href="{CSS_BASE_PATH}/{tagname}.css">
utils.StyleHelper.loadComponentStyle(this);

The base path defaults to "css" and can be set through the global variable named by utils.StyleHelper.CSS_BASE_PATH_VAR.

utils.WeakData

A per-reference data store backed by a WeakMap; data is garbage-collected once the reference goes away — ideal for attaching state to DOM nodes.

const store = new utils.WeakData();

store.value(node, "count", 1);   // set
store.value(node, "count");      // get -> 1
store.value(node, "count", undefined); // delete
store.destroy(node);             // drop everything for `node`

utils.DefineComponentHelper

The define function is also reachable here, together with toNodeName:

utils.DefineComponentHelper(MyComponent);         // = define(MyComponent)

Settings

SETTING holds mutable runtime settings. Change them before components are created:

import { SETTING } from "@default-js/defaultjs-html-components";

SETTING.eventSeparator = ":";    // events become "d-foo:change", …

Helpers

createUUID(prefix?, suffix?)

Generate a document-unique id (retries until unused):

import { createUUID } from "@default-js/defaultjs-html-components";

const id = createUUID("id-");    // -> "id-…unique…"

Ready()

Factory for a lazy promise whose resolution you control and that exposes a resolved flag — the same primitive that backs a component's ready:

import { Ready } from "@default-js/defaultjs-html-components";

const gate = Ready();
someEvent.then(() => gate.resolve());
await gate;

Usage in the browser (global build)

The bundled browser build registers everything under a global namespace:

<script src="dist/defaultjs-html-components.js"></script>
<script>
  const { Component, define, componentBaseOf, utils, SETTING } =
    window.defaultjs.html.components;
</script>

TypeScript

Type definitions ship with the package (index.d.ts), so imports are typed out of the box:

import { Component, componentBaseOf, ComponentOptions } from "@default-js/defaultjs-html-components";

class MyButton extends Component {
  static get NODENAME() { return "d-my-button"; }
  constructor() { super({ shadowRoot: true } satisfies ComponentOptions); }
}

The DOM-extension methods added by defaultjs-extdom (attr, find, trigger, …) are provided by that package's own typings, not by this one.


API overview

| Export | Kind | Description | | ----------------- | -------- | -------------------------------------------------------------------------------- | | Component | class | Base class for HTMLElement-based components. | | componentBaseOf | function | Build a component base class for any HTML element type. | | define | function | Register a component as a custom element (uses its NODENAME). | | createUUID | function | Create a document-unique id. | | Ready | function | Create a controllable, lazy promise. | | SETTING | object | Runtime settings (eventSeparator). | | utils | object | EventHelper, NodeHelper, StyleHelper, WeakData, DefineComponentHelper. |


License

MIT © Frank Schüler