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

@brandup/ui

v2.0.6

Published

Lightweight UI framework on top of the DOM.

Readme

brandup-ui

Build Status

Installation

Install NPM package @brandup/ui.

npm i @brandup/ui@latest

UIElement

UIElement is a wrapper for HTMLElement that lets you attach your own business logic to it.

Features:

  • Handling of commands declared in the markup of the HTMLElement that is bound to the UIElement.
  • Subscribing to events through EventEmitter, which UIElement extends.
abstract class UIElement extends EventEmitter {
    abstract typeName: string;
    readonly element: HTMLElement | undefined;

    static hasElement(elem: HTMLElement): boolean;

    protected setElement(elem: HTMLElement): void;

    registerCommand(name: string, execute: CommandExecuteFunction, canExecute?: CommandCanExecuteFunction): this;
    hasCommand(name: string): boolean;

    protected _onRenderElement(elem: HTMLElement): void;
    protected _onCanExecCommand(name: string, elem: HTMLElement): boolean;

    effectScope(): EffectScope;
    destroy(): void;

    toString(): string;
}

UIElement is an abstract class. A subclass sets typeName and binds a DOM element via setElement. An element can be bound only once; binding again (or binding an element that already belongs to another instance) throws an exception.

import { UIElement } from "@brandup/ui";

class MyWidget extends UIElement {
    typeName = "MyWidget";

    constructor(elem: HTMLElement) {
        super();
        this.setElement(elem);
    }

    protected _onRenderElement(elem: HTMLElement) {
        // initialize the markup
    }
}

The HTMLElement.prototype.ui extension lets you bind a UIElement through a factory and returns the element itself. It is opt-in (not installed on import) — call enableElementExtensions() once before using it:

import { enableElementExtensions } from "@brandup/ui";

enableElementExtensions();
document.getElementById("widget")!.ui(elem => new MyWidget(elem));

The bound UIElement is available on the node through the node.uielement property.

Element bound at construction

On the base UIElement, element is HTMLElement | undefined because the element may be bound later (an Application, for example, binds its element on run). When a component always receives its element in the constructor, extend UIElementBound instead — it binds the element immediately, so element is typed HTMLElement (never undefined):

import { UIElementBound } from "@brandup/ui";

class MyWidget extends UIElementBound {
    constructor(elem: HTMLElement) {
        super("MyWidget", elem); // typeName + element
    }
}

const w = new MyWidget(document.createElement("div"));
w.element.focus(); // element: HTMLElement — no ?./!

UI commands

UIElement lets you register command handlers, which are declared in the markup through the data-command attribute.

<button data-command="send">Send</button>
this.registerCommand("send", (context: CommandContext) => {
    context.target.innerHTML = "ok";
});

You can register asynchronous command handlers — just return a Promise:

this.registerCommand("command1-async", (context: CommandContext) => {
    return new Promise<void>(resolve => {
        context.target.innerHTML = "Loading...";
        window.setTimeout(() => {
            context.target.innerHTML = "Ok";
            resolve();
        }, 2000);
    });
});

The third argument, canExecute, lets you restrict execution of the command:

this.registerCommand(
    "submit",
    (context) => { /* ... */ },
    (context) => context.target.dataset.enabled === "true"
);

Commands are triggered by the click event. The handler is looked up by walking up the DOM from the element with the data-command attribute to the nearest UIElement in which that command is registered. The global click listener must be enabled once via initUICommands()Application does this automatically (see Command click handler).

While an asynchronous command is running, the executing CSS class is added to the target element (and removed once the Promise settles).

Command type signatures:

type CommandExecuteFunction = (context: CommandContext) => void | Promise<void | any>;
type CommandCanExecuteFunction = (context: CommandContext) => boolean;

interface CommandContext {
    /** HTMLElement on which the command is executed. */
    target: HTMLElement;
    /** UIElement in which the command handler is registered. */
    uiElem: UIElement;
    /** Don't stop the click event chain of target. */
    transparent?: boolean;
}

interface CommandResult {
    status: CommandExecStatus; // "disallow" | "already" | "success"
    context: CommandContext;
}

Before executing a command, UIElement triggers the command event with CommandEventArgs arguments ({ element, name }).

UI Events

UIElement extends the EventEmitter class.

class EventEmitter<TEvents = EventMap> {
    on<K extends keyof TEvents & string>(eventName: K, callback: TEvents[K], context?: any): this;
    once<K extends keyof TEvents & string>(eventName: K, callback: TEvents[K], context?: any): this;
    off<K extends keyof TEvents & string>(eventName?: K | "all" | null, callback?: TEvents[K] | EventCallbackFunc | null, context?: any | null): this;

    protected listenTo(source: EventEmitter<any>, eventName: string, callback: EventCallbackFunc): this;
    protected listenToOnce(source: EventEmitter<any>, eventName: string, callback: EventCallbackFunc): this;
    protected stopListening(source?: EventEmitter<any>, eventName?: string, callback?: EventCallbackFunc): this;

    protected trigger<K extends keyof TEvents & string>(eventName: K, ...args: Parameters<TEvents[K]>): this;
}

Typed events

The optional TEvents type parameter is an event map ({ eventName: (args) => void }) that gives a subclass strongly-typed event names, callback signatures and trigger arguments. It defaults to a loose map, so untyped usage keeps working.

interface CounterEvents {
    increment: (by: number) => void;
    reset: () => void;
}

class Counter extends EventEmitter<CounterEvents> {
    add(n: number) {
        this.trigger("increment", n);   // ✅ ok
        // this.trigger("increment", "x"); // ❌ string is not number
        // this.trigger("nope");           // ❌ unknown event
    }
}

const c = new Counter();
c.on("increment", by => console.log(by.toFixed(0))); // by: number
// c.on("unknown", () => {});                          // ❌ unknown event

UIElement is itself generic — UIElement<TEvents> merges TEvents with the built-in command/rendered/destroy events, so subclasses can add their own typed events:

class MyWidget extends UIElement<{ ready: () => void }> {
    // on/trigger accept "command", "destroy" AND "ready"
}

Subscribing to and unsubscribing from events:

widget.on("command", (args: CommandEventArgs) => {
    console.log("executing", args.name);
});

// one-time handler
widget.once("destroy", () => console.log("destroyed"));

// unsubscribe (filters can be omitted — an omitted filter matches anything)
widget.off("command");

The special event name "all" receives every triggered event.

The protected listenTo / listenToOnce methods subscribe one emitter to another's events and track the subscription so it can be released via stopListening. On destroy(), all of a UIElement's subscriptions are removed automatically.

UIElement lifecycle

destroy()

destroy() cleans up the element completely:

  • Fires the "destroy" event.
  • Stops all event subscriptions.
  • Cascades to every nested UIElement found in the subtree (deepest descendants first), so you never need to destroy children manually.
  • Stops all reactive bind/bindEach effects rendered inside the element.
  • Detaches the UIElement from its DOM node (clears the data-uiElement attribute and the uielement property).
const parent = new ParentWidget(parentElem); // contains child UIElements
parent.destroy(); // automatically destroys all nested UIElements too

Auto-destroy on DOM removal

When a bound element is removed from the document after having been connected to it, destroy() is called automatically. This works for all nested UIElements too — removing a parent node triggers the full destroy cascade.

const w = new MyWidget(elem);
document.body.appendChild(elem);

elem.remove(); // destroy() fires automatically on next microtask

If the element was never connected to the document (e.g. built in memory and then discarded), auto-destroy does not fire — only mounted-then-removed elements are watched.

Command click handler

Commands are dispatched by a single global click listener on window. It is not registered automatically on import (so the command system can be tree-shaken away when unused) — call initUICommands() once during startup. It is idempotent and a no-op without a DOM. Application.run() (from @brandup/ui-app) calls it for you, so you only need it when using UIElement commands without an Application:

import { initUICommands, destroyUI } from "@brandup/ui";

initUICommands(); // enable command handling
// ...
destroyUI();      // remove the listener on app teardown or HMR disposal

DOM helpers

Previously published as the separate @brandup/ui-dom package, now merged into @brandup/ui.

All DOM helpers are available through the DOM object.

import { DOM } from "@brandup/ui";

const DOM = {
    // Finding elements
    getById<T extends HTMLElement = HTMLElement>(id: string): T | null;
    getByClass<T extends HTMLElement = HTMLElement>(container: Element, className: string): T | null;
    getByName<T extends HTMLElement = HTMLElement>(name: string): T | null;
    getElementByTagName<T extends HTMLElement = HTMLElement>(container: Element, tagName: string): T | null;
    getElementsByTagName(container: Element, tagName: string): HTMLCollectionOf<Element>;
    queryElement<T extends HTMLElement = HTMLElement>(container: Element, query: string): T | null;
    queryElements<T extends HTMLElement = HTMLElement>(container: Element, query: string): NodeListOf<T>;

    // Navigating sibling elements
    nextElement<T extends HTMLElement = HTMLElement>(current: Element): T | null;
    prevElement<T extends HTMLElement = HTMLElement>(current: Element): T | null;
    nextElementByClass<T extends HTMLElement = HTMLElement>(current: Element, className: string): T | null;
    prevElementByClass<T extends HTMLElement = HTMLElement>(current: Element, className: string): T | null;

    // CSS classes
    addClass(container: Element | null | undefined, selectors: string, cssClass: CssClass): void;
    removeClass(container: Element | null | undefined, selectors: string, cssClass: CssClass): void;

    // Clearing
    empty(container: Element | null | undefined): void;

    // Creating elements
    tag<T extends keyof HTMLElementTagNameMap>(tagName: T, options?: ElementOptions | null, ...children: TagChildrenLike[]): HTMLElementTagNameMap[T];
    tag<T extends keyof HTMLElementTagNameMap>(tagName: T, firstChild: TagFirstChild, ...children: TagChildrenLike[]): HTMLElementTagNameMap[T];
};

Creating HTML elements

DOM.tag creates an element from a tag name. The remaining arguments are children or options:

  • Options — pass null (no options) or an ElementOptions plain object as the second argument.
  • Children — any other value in the second position (string, number, Element, Binding, BindingEach, Promise, function, array) is treated as the first child, so the options argument can be omitted entirely.
// No options, no children
DOM.tag("div");

// Options object (id, class, dataset, styles, events, arbitrary attributes)
DOM.tag("div", { class: "box", id: "main" });

// null → no options, children follow
DOM.tag("div", null, "<p>test</p>");

// String as second arg → inserted as HTML child (NOT a CSS class)
DOM.tag("div", "<b>hello</b>");
DOM.tag("p", "plain text");

// Number or boolean as second arg → text child
DOM.tag("span", 42);

// Element/UIElement child — no null needed
DOM.tag("div", DOM.tag("span", "child"));
DOM.tag("div", new MyWidget(DOM.tag("span")));

// Multiple children
DOM.tag("ul", null, DOM.tag("li", "1"), DOM.tag("li", "2"));

// Children in an array
DOM.tag("ul", [DOM.tag("li", "1"), DOM.tag("li", "2")]);

// Factory function: receives the container element
DOM.tag("div", (elem) => { elem.id = "x"; });
DOM.tag("div", () => DOM.tag("span", "child"));

// Promise child — appended once it resolves
DOM.tag("div", fetch("/fragment").then(r => r.text()));

The full ElementOptions object:

interface ElementOptions {
    id?: string;                  // id attribute
    class?: CssClass;             // CSS class(es): a string or an array of strings
    command?: string;             // data-command
    dataset?: ElementData;        // arbitrary data-* attributes
    events?: ElementEvents;       // event handlers keyed by lowercase name
    styles?: ElementStyles;       // inline styles (Partial<CSSStyleDeclaration>)
    [name: string]:               // any other key is a plain attribute:
        | string | number | boolean | object | null | undefined;
    // null → empty attribute, object → JSON.stringify, undefined → ignored
}

Reactivity

A small fine-grained reactivity layer (Vue/MobX-style) with auto-tracking, plus DOM.tag bindings that update the DOM in place.

import { reactive, effect, computed, nextTick, untrack, bind, bindEach, DOM } from "@brandup/ui";

reactive / effect / computed

reactive(obj) returns a deep reactive proxy: reads are tracked and writes notify the effects that read them.

const state = reactive({ first: "Ada", last: "Lovelace", tags: ["math"] });

// effect re-runs when any property it reads changes
effect(() => console.log(state.first));

// computed: lazily cached, recomputes only when its dependencies change
const full = computed(() => `${state.first} ${state.last}`);

state.first = "Augusta"; // schedules the effect and invalidates `full`
  • Deep: nested objects and arrays are reactive (state.tags.push(...) is tracked).
  • Dynamic dependencies: an effect only depends on the properties it actually reads on its last run (useA ? a : b re-subscribes).
  • Batched: effect re-runs are coalesced on the microtask queue, so multiple synchronous writes trigger a single run. Await nextTick() to observe the result:
state.first = "A";
state.last = "B";
await nextTick(); // effects have now re-run once

untrack

untrack(fn) runs a function without recording any reactive reads as dependencies. Use it when you need to read reactive state inside an effect without creating a dependency on that read:

import { untrack } from "@brandup/ui";

effect(() => {
    const items = state.list;          // tracked — effect re-runs when list changes
    const config = untrack(() => state.config); // not tracked — config changes won't re-run this effect
    render(items, config);
});

Binding DOM with bind

bind(() => expr) is a reactive tag child. The expression is tracked and re-rendered in place when its reactive state changes — text values reuse a text node (rendered via textContent, so safe from HTML injection), element/UIElement values swap the node.

const state = reactive({ name: "Alice", online: true });

const el = DOM.tag("div", null,
    "Hi, ", bind(() => state.name), "! ",
    bind(() => state.online ? DOM.tag("b", null, "online") : "offline")
);

state.name = "Bob"; // the text updates on the next tick

Binding an array property with bindEach

bindEach is a reactive tag child — used exactly like bind — that renders a keyed list with minimal DOM updates. Each item is identified by a stable key; when the array changes, only new, removed, or reordered nodes are touched.

import { reactive, bindEach, bind, nextTick, DOM } from "@brandup/ui";

const state = reactive({
    users: [
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
        { id: 3, name: "Charlie" },
    ]
});

const list = DOM.tag("ul", "user-list",
    bindEach(
        () => state.users,          // reactive item source (tracked)
        user => user.id,            // stable key — identifies each node across re-renders
        user => DOM.tag("li", null, // render one item → Element (called once per key)
            bind(() => user.name)   // bind() inside render for fine-grained per-item updates
        )
    )
);

Array mutations are tracked — the list reconciles on the next tick:

state.users.push({ id: 4, name: "Diana" }); // inserts one new <li>
state.users.splice(1, 1);                   // removes the <li> for id=2
state.users.unshift(state.users.pop()!);    // moves last node to front, no re-render
state.users = [{ id: 1, name: "Alice" }];   // full reassignment — removes all but id=1

await nextTick(); // DOM is up to date

Because render is called once per key and runs untracked, reads inside it do not create dependencies on the list reconciler. Use bind() inside render so individual property changes update only the affected node — not the whole list:

// ✅  only the text node re-renders when user.name changes
user => DOM.tag("li", null, bind(() => user.name))

// ⚠️  name changes have no effect — render is untracked and never called again for existing keys
user => DOM.tag("li", null, user.name)

The binding stops automatically once its rendered nodes leave the document — whether the container is removed or just cleared/replaced (same lifecycle as bind).

Disposal

Bindings hold a reactive effect that must be stopped to avoid leaks. This is handled automatically in common cases:

  • UIElement.destroy() stops every bind/bindEach effect in the subtree and cascades to nested UIElements — no manual wiring needed.
  • A binding stops itself once its node has been mounted into the document and then removed from it.
  • Removing a bound element from the document also calls destroy() automatically (see UIElement lifecycle).
class Widget extends UIElementBound {
    constructor(elem: HTMLElement) {
        super("widget", elem);
        elem.append(DOM.tag("span", null, bind(() => state.name)));
    }
}
const w = new Widget(document.createElement("div"));
w.destroy(); // stops bind() effects, cascades to nested UIElements

For effects and bindings outside a UIElement, or to group them explicitly, use EffectScope:

import { effectScope } from "@brandup/ui";

const scope = effectScope();
const view = scope.run(() => DOM.tag("div", null, bind(() => state.name)));
scope.stop(); // stops every effect/binding created inside the scope

UIElement.effectScope() returns a scope that is stopped automatically when the element is destroyed:

class Widget extends UIElementBound {
    constructor(elem: HTMLElement) {
        super("widget", elem);
        this.effectScope().run(() => {
            elem.append(DOM.tag("span", null, bind(() => state.name)));
        });
    }
}

Constants

The names of DOM attributes, properties, and CSS classes are exported as the UICONSTANTS namespace:

import { UICONSTANTS } from "@brandup/ui";

UICONSTANTS.ElemAttributeName;              // "uiElement"   — data attribute holding the typeName
UICONSTANTS.ElemPropertyName;               // "uielement"   — property on the DOM element referencing the UIElement
UICONSTANTS.CommandAttributeName;           // "command"     — data attribute of the command
UICONSTANTS.CommandExecutingCssClassName;   // "executing"   — class applied while an async command is running