Vanilla DOM Adapter

The @banksia/signals/vanilla subpath provides zero-dependency DOM synchronization helpers for micro-frontends, lightweight landing pages, and standalone scripts without any UI framework overhead.


Installation

pnpm add @banksia/signals

(No external or framework peer dependencies required!)


bindText(element, getter)

Directly synchronizes an HTML element's textContent with a reactive expression:

import { signal } from "@banksia/signals";
import { bindText } from "@banksia/signals/vanilla";

const count = signal(0);
const span = document.querySelector<HTMLSpanElement>("#counter")!;

// Automatically sets span.textContent and updates whenever count.value changes:
const unbind = bindText(span, () => `Current count: ${count.value}`);

count.value = 10; // Updates DOM directly

// Call unbind to tear down subscription:
unbind();

bindDOM(element, updater)

Provides full control to mutate element classes, styles, attributes, or properties reactively. The updater callback executes once immediately upon binding, and re-executes whenever reactive signals or proxy properties read within it mutate:

import { makeReactive } from "@banksia/signals";
import { bindDOM } from "@banksia/signals/vanilla";

const uiState = makeReactive({
  loading: false,
  error: null as string | null,
  theme: "dark",
});

const submitBtn = document.querySelector<HTMLButtonElement>("#submit-btn")!;

const unbind = bindDOM(submitBtn, (btn) => {
  btn.disabled = uiState.loading;
  btn.classList.toggle("is-loading", uiState.loading);
  btn.setAttribute("data-theme", uiState.theme);
});

Recipe: Two-Way Form Input Binding

To synchronize an <input> element two-way with a reactive signal or domain store property, combine bindDOM with a native input event listener:

import { signal } from "@banksia/signals";
import { bindDOM } from "@banksia/signals/vanilla";

export function bindTwoWayInput(
  input: HTMLInputElement,
  textSignal: ReturnType<typeof signal<string>>,
) {
  // 1. Signal -> Input DOM:
  const unbindDOM = bindDOM(input, (el) => {
    if (el.value !== textSignal.value) {
      el.value = textSignal.value;
    }
  });

  // 2. Input DOM -> Signal:
  const onInput = () => {
    textSignal.value = input.value;
  };
  input.addEventListener("input", onInput);

  // Return combined disposal function:
  return () => {
    unbindDOM();
    input.removeEventListener("input", onInput);
  };
}

// Usage:
const search = signal("");
const searchBox = document.querySelector<HTMLInputElement>("#search-box")!;
const unbindInput = bindTwoWayInput(searchBox, search);

TypeScript Signatures

export function bindDOM<T extends Element>(
  element: T,
  updater: (el: T) => void,
): () => void;

export function bindText(
  element: HTMLElement,
  getter: () => string | number,
): () => void;