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

@supercat1337/store2-dom

v1.1.0

Published

DOM binding utilities for @supercat1337/store2 – reactive atoms, collections, and computed values

Readme

@supercat1337/store2-dom

DOM binding utilities for reactive stores — seamlessly connect @supercat1337/store2 to the DOM.

npm version License: MIT Bundle Size


Why store2-dom?

  • Declarative DOM bindings – no more manual addEventListener and textContent updates.
  • Automatic cleanup – bindings unsubscribe when elements are removed from the DOM (autoDisconnect: true by default) using a lightweight isConnected check.
  • Framework‑agnostic – works with vanilla JS, React, Vue, or any DOM environment.
  • Supports AbortSignal – easy integration with component lifecycles.
  • Tiny footprint – ~13 KB core, depends only on @supercat1337/store2.

Installation

npm install @supercat1337/store2-dom

Quick Start

<!-- index.html -->
<input type="text" id="name-input" />
<p id="greeting"></p>
<button id="reset-btn">Reset</button>
import { atom, computed } from '@supercat1337/store2';
import { bindToInput, bindToText } from '@supercat1337/store2-dom';

const name = atom('World');
const greeting = computed(() => `Hello, ${name.value}!`);

bindToInput(document.getElementById('name-input'), name);
bindToText(document.getElementById('greeting'), greeting);

// Reset button – directly mutates the atom
document.getElementById('reset-btn').addEventListener('click', () => {
    name.value = 'World';
});

Now typing in the input automatically updates the greeting paragraph — reactive DOM binding in a few lines.


Integration with deepReactive (from @supercat1337/store2-deep)

store2-dom works seamlessly with deep reactive objects created via deepReactive. Since deepReactive uses per‑property atoms internally, you can read a nested property using a read‑only computed and bind it to the DOM.

Recommended pattern:

  1. Create a computed getter that reads the nested property.
  2. Use bindToProperty or bindToText to update the DOM when the computed changes.
  3. For user input, listen to DOM events and mutate the proxy directly — no need to create separate atoms.
import { deepReactive } from '@supercat1337/store2-deep';
import { bindToProperty } from '@supercat1337/store2-dom';
import { computed } from '@supercat1337/store2';

const state = deepReactive({ user: { name: 'Alice', age: 30 } });

// Read‑only computed for a nested property
const nameComputed = computed(() => state.user.name);

// Bind to input.value – updates DOM when state changes
const input = document.getElementById('name');
bindToProperty(input, nameComputed, 'value');

// Two‑way: write back to the proxy on user input
input.addEventListener('input', () => {
    state.user.name = input.value;
});

Why not bindToInput?
bindToInput expects an Atom<string|number> with a setter. computed is read‑only and has no setter, so bindToInput cannot update it. Use bindToProperty for read‑only computed values and handle the reverse direction via DOM events.

💡 Alternative: If you prefer a more declarative style, you can use reaction to manually update the DOM:

reaction(
    () => state.user.name,
    name => {
        input.value = name;
    }
);

However, bindToProperty handles unsubscription automatically (via autoDisconnect and signal) and is cleaner for simple property bindings.

This pattern gives you a clear unidirectional data flow: state → DOM via computed, DOM → state via events.


Core Concepts

| Concept | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | One‑way binding | Updates DOM when reactive value changes (store → DOM). | | Two‑way binding | Syncs DOM events back to the reactive item (store ↔ DOM). | | List binding | Efficiently renders a Collection into a container with minimal DOM operations. | | Auto‑disconnect | Automatically cleans up subscriptions when the target element is removed from DOM (checks element.isConnected on each update). | | AbortSignal | Use signal option to unbind when an AbortController is aborted. |


One‑Way Bindings

| Function | Description | | -------------------------------------------------------- | -------------------------------------------------------------------------- | | bindToAttribute(element, reactive, attrName, options?) | Sets/removes an attribute based on reactive string or null. | | bindToClassString(element, reactive, options?) | Sets element.className from a reactive string. | | bindToCssClass(element, reactive, className, options?) | Toggles a CSS class based on boolean value. invert flips logic. | | bindToDisabled(element, reactive, options?) | Sets element.disabled from a reactive boolean. | | bindToHtml(element, reactive, options?) | Sets element.innerHTML from a reactive string/number. | | bindToProperty(element, reactive, propName, options?) | Sets any DOM property from a reactive value. | | bindToShow(element, reactive, options?) | Toggles visibility via a CSS class (default d-none). | | bindToStyle(element, reactive, options?) | Sets element.style.cssText from a string or applies an object of styles. | | bindToDataset(element, reactive, options?) | Sets data-* attributes from a reactive object (replaces all). | | bindToText(element, reactive, options?) | Sets element.textContent from a reactive string/number. |


Two‑Way Bindings

| Function | Description | | ------------------------------------------------------- | --------------------------------------------------------------------------- | | bindToCheckbox(checkbox, reactive, options?) | Syncs checkbox checked with a boolean atom. | | bindToCheckboxGroup(checkboxes, collection, options?) | Syncs a group of checkboxes with a collection of strings (selected values). | | bindToInput(input, reactive, options?) | Syncs input/textarea value with a string/number atom. | | bindToRadioGroup(radios, reactive, options?) | Syncs a group of radios (same name) with a string atom. | | bindToSelect(select, reactive, options?) | Syncs a single‑select with a string atom. | | bindToSelectMultiple(select, collection, options?) | Syncs a multi‑select with a collection of strings. |

⚠️ Important: Two-way bindings expect an Atom for single values and a Collection for multiple values. Passing a Computed will throw a TypeError. This ensures correct data flow and prevents accidental writes to read-only values.


List Binding (bindToList)

Efficiently render a reactive Collection into a container.

import { collection } from '@supercat1337/store2';
import { bindToList } from '@supercat1337/store2-dom';

const todos = collection([{ id: 1, text: 'Learn store2' }]);

bindToList(
    document.getElementById('todo-list'),
    todos,
    // onUpdateItem – called on item creation and every update
    (helper, { itemElement, value, oldValue }) => {
        const span = itemElement.querySelector('span');
        if (helper.getDiffs({ text: value }, { text: oldValue }).text) {
            span.textContent = value.text;
        }
    },
    // createItem – optional custom element factory (uses first child as template if omitted)
    null
);
  • onUpdateItem: (helper: ListItemHelper, details: ListItemUpdateContext<T>) => void – called when an item is created or updated. Use helper.getDiffs() for partial updates.
  • createItem?: (helper: ListItemHelper) => HTMLElement – optional function to create a new DOM element. If not provided, the first child of the container is cloned as a template.
  • Returns an unsubscribe function.

Global Options & Per‑Binding Options

All bindings accept an options object:

| Option | Type | Default | Description | | ------------------------ | ----------- | ----------- | ---------------------------------------------------------------------------------- | | debounceTime | number | 0 | Debounce time (ms) for store subscription. | | autoDisconnect | boolean | true | Automatically unbind when element is removed from DOM (checked via isConnected). | | signal | AbortSignal | undefined | Unbind when signal is aborted. | | event (two‑way) | string | depends | Custom event name for DOM updates. | | lazy (input) | boolean | false | If true, listens to change instead of input. | | invert (class toggles) | boolean | false | If true, class is applied when value is false. | | hideClassName (show) | string | 'd-none' | CSS class used to hide the element. |

You can change defaults globally:

import { globalOptions } from '@supercat1337/store2-dom';
globalOptions.debounceTime = 100;
globalOptions.autoDisconnect = false;

⚠️ Important Notes / Known Limitations

  • Nested mutations are not tracked – always use immutable updates or makeAutoObservable (see store2 docs).
  • Destructuring a reactive value breaks tracking – always use reactive.value directly inside bindings.
  • autoDisconnect checks element.isConnected on each update – if the element is removed but the reactive value never changes again, cleanup will only happen on the next update. For guaranteed cleanup, use signal or call the returned unsubscribe function.
  • Two‑way bindings may cause loops – ensure your reactive logic doesn't update the same atom in response to its own change.
  • bindToList performs a full rerender on collection replacement – if you replace the entire collection.value with a new array, it will rebuild the list. Use mutation methods (push, setItem, etc.) for incremental updates.

Integration with Frameworks

React

import { atom } from '@supercat1337/store2';
import { bindToInput } from '@supercat1337/store2-dom';
import { useEffect, useRef } from 'react';

// Create atom outside component to avoid recreation on each render
const nameAtom = atom('React');

function NameInput() {
    const inputRef = useRef(null);

    useEffect(() => {
        const unsub = bindToInput(inputRef.current, nameAtom);
        return unsub;
    }, []);

    return <input ref={inputRef} />;
}

Alternatively, if you need the atom to be instance‑specific, use useRef with lazy initialization:

function NameInput() {
    const inputRef = useRef(null);
    const nameRef = useRef(null);

    if (!nameRef.current) {
        nameRef.current = atom('React');
    }

    useEffect(() => {
        const unsub = bindToInput(inputRef.current, nameRef.current);
        return unsub;
    }, []);

    return <input ref={inputRef} />;
}

Vue (Composition API)

<template>
    <input ref="inputRef" />
</template>

<script setup>
import { atom } from '@supercat1337/store2';
import { bindToInput } from '@supercat1337/store2-dom';
import { ref, onMounted, onUnmounted } from 'vue';

const name = atom('Vue');
const inputRef = ref(null);
let unsubscribe;

onMounted(() => {
    unsubscribe = bindToInput(inputRef.value, name);
});
onUnmounted(() => unsubscribe?.());
</script>

For better integration, consider using the signal option with an AbortController.


Utilities

getDiffs(newObject, oldObject, customCompareFunction?)

Returns an object with the same keys as newObject, value true if the property is new or changed.

getElement(selector, type?, root?)

Finds the first element matching a CSS selector. Throws if not found.

  • selector: string – CSS selector.
  • type?: new (...args: any[]) => T – optional constructor for type checking.
  • root?: Document | Element – root element to search within (default document).
import { getElement } from '@supercat1337/store2-dom';

const input = getElement('#my-input', HTMLInputElement);
const span = getElement('.my-span', HTMLSpanElement, container);

getElementById(id, type?, root?)

Same as getElement, but by ID.

import { getElementById } from '@supercat1337/store2-dom';

const div = getElementById('my-div', HTMLDivElement);

globalOptions

Global defaults object that you can mutate:

import { globalOptions } from '@supercat1337/store2-dom';
globalOptions.debounceTime = 100;
globalOptions.autoDisconnect = false;

TypeScript

The package ships with its own .d.ts files. Import types if needed:

import type {
    BinderOptions,
    ListItemHelper,
    ListItemUpdateContext,
} from '@supercat1337/store2-dom';

License

MIT © 2025–2026 Albert Bazaleev