Skip to content

doeixd/dom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bundlejs npm version License Ask DeepWiki


DOM

A modern utility library for the DOM and frontend development. It's a collection of type-safe, composable functions for the 80% of tasks you do every day, without the overhead of a framework.

Key Features:

  • Lightweight & Zero-Dependency: Tiny and tree-shakeable.
  • Type-Safe by Default: Automatically infers element and event types from your code.
  • Null-Safe: Stop writing if (element) checks. Functions handle missing elements gracefully.
  • Composable by Design: Small, focused utilities that work together seamlessly.

Installation

npm install @doeixd/dom

Or With ESM.sh (No Build Step)

Use directly in the browser without npm or build tools:

<script type="module">
  // Import only what you need - automatic tree-shaking
  import { find, modify, on } from 'https://esm.sh/@doeixd/dom';
  
  const button = find('button');
  modify(button)({ text: 'Click me!' });
  on(button)('click', () => alert('Clicked!'));
</script>

Tree-Shaking with ESM.sh: Use the ?exports parameter to bundle only specific functions:

<script type="module">
  // Only bundles find, modify, and on (~2KB instead of ~15KB)
  import { find, modify, on } from 'https://esm.sh/@doeixd/dom?exports=find,modify,on';
</script>

Quick Start

Here’s a taste of how the library feels. Let's find a button, update it, and handle a click.

import { find, modify, on, addClass } from '@doeixd/dom';

// 1. Find an element (types are inferred)
const btn = find('button.submit'); // HTMLButtonElement | null

// 2. Modify its properties
modify(btn)({
  text: 'Submit Order',
  disabled: false
});
addClass(btn)('btn-primary');

// 3. Listen for an event (returns a cleanup function)
const stopListening = on(btn)('click', () => {
  console.log('Order submitted!');
});

Design Philosophy

The library is built on a few simple, powerful ideas.

1. A Predictable API: Action(Target)(Config) Most functions follow a "target-first" pattern. You first specify what you're working with, then you describe the action.

//   Action(Target)  (Configuration)
modify(myButton)  ({ text: 'Hello' });
addClass(myButton)('active');

This design makes your code highly reusable. You can prepare an action and apply it to many elements:

const makeActive = addClass('active');
buttons.forEach(makeActive);

(This pattern is also known as currying, which is what makes composing functions so easy.)

2. Flexible Usage Styles While the target-first style is great for composition, you can also use a more direct, imperative style or a fluent, jQuery-like style. Choose what fits the situation best.

// A) Functional (Recommended)
modify(btn)({ text: 'Click' });

// B) Imperative (Direct)
modify(btn, { text: 'Click' });

// C) Fluent (jQuery-like)
$('button').modify({ text: 'Click' }).addClass('active');

The Component Pattern

For complex applications, @doeixd/dom provides a lightweight component architecture. It bridges the gap between low-level DOM manipulation and high-level frameworks like React or Vue, without the build steps or virtual DOM overhead.

defineComponent

This function creates a self-contained logic unit attached to a DOM element. It solves three major problems in Vanilla JS development:

  1. Ref Management: Automatically maps data-ref elements to variables.
  2. Lifecycle Management: Automatically cleans up event listeners and observers when the component is destroyed.
  3. State Sync: Provides a simple way to sync JavaScript state with DOM data attributes.

Example: Counter Component

HTML:

<div id="counter">
  <span data-ref="display">0</span>
  <button data-ref="btn">Increment</button>
</div>

TypeScript:

import { defineComponent } from '@doeixd/dom';

// 1. Define Types (Optional)
interface Refs { display: HTMLElement; btn: HTMLButtonElement; }
interface State { count: number; }

// 2. Define Logic
const Counter = defineComponent<any, Refs, any, State>('#counter', (ctx) => {
  const { display, btn } = ctx.refs;

  // Initialize State (sets data-count="0" in DOM)
  ctx.state.count = 0;

  // Event Listener (auto-cleaned on destroy)
  ctx.on('click', btn, () => {
    ctx.state.count++;
  });

  // Reactive Watcher (runs when state changes)
  ctx.watch('count', (val) => {
    display.textContent = String(val);
  });
  
  // Return Public API
  return {
    reset: () => { ctx.state.count = 0; }
  };
});

// 3. Usage
// Counter.reset();
// Counter.destroy(); // Removes all listeners

The ComponentContext (ctx)

The ctx object passed to your setup function provides scoped, auto-cleaning utilities:

Property Description
ctx.root The root element of the component.
ctx.refs Object map of elements with data-ref="name".
ctx.groups Object map of element lists with data-ref="name".
ctx.state Proxy object for reading/writing data-* attributes.
ctx.on Add event listener (auto-removed on destroy).
ctx.watch Watch state changes.
ctx.bind Two-way bind an input to a state key.
ctx.observe Add Intersection/Resize observer (auto-disconnected).
ctx.effect Register arbitrary cleanup logic.

Learn More

For comprehensive guides on components:

  • Component Documentation - Complete guide to components, TypeScript integration, lifecycle, state management, real-world examples, and best practices
  • Advanced Component Patterns - The binder pattern, custom hooks, plugin systems, state management strategies, dynamic components, and testing

The Toolkit: A Practical Guide

DOM Querying & Traversal

Function Description Example
find Find the first matching element. const btn = find(document)('.submit-btn');
findAll Find all matches as a standard Array. const items = findAll(list)('li');
require Find element or throw if not found. const btn = require('button');
closest Find the closest matching ancestor. const card = closest(btn)('.card');
exists Check if an element exists. const exists = exists('.submit-btn');
has Check if an element contains a descendant. const has = has('.card')('.submit-btn');
index Get the index of an element. const idx = index(btn);
siblings Get all sibling elements. const sibs = siblings(btn);
attr Get or set element attributes. const id = attr(el)('id'); attr(el)('id', '123');
prop Get or set element properties. const checked = prop(input)('checked');
Traverse.parent Get the parent element. const parent = Traverse.parent(el);
Traverse.children Get child elements as an array. const kids = Traverse.children(el);
Traverse.siblings Get all sibling elements. const sibs = Traverse.siblings(el);
Traverse.next Get the next sibling. const next = Traverse.next(el);
Traverse.prev Get the previous sibling. const prev = Traverse.prev(el);

DOM Manipulation & Creation

Function Description Example
modify Declaratively set text, class, dataset, attr, value, etc. modify(el)({ text: 'Hi', disabled: true })
set Alias for modify. set(el)({ text: 'Hi' })
css Apply inline styles. css(el)({ color: 'red', opacity: '1' })
tempStyle Apply styles temporarily, returns revert function. const revert = tempStyle(el)({ opacity: '0.5' });
el Create a new element with props and children. el('div')({ class:{box:1} })([child])
html Create an element from a template literal. const div = html`<div>${name}</div>`;
htmlMany Create a DocumentFragment from HTML. const frag = htmlMany`<li>A</li><li>B</li>`;
append Append nodes or text to an element. append(parent)(child1, 'text')
prepend Prepend nodes or text to an element. prepend(list)(newItem)
after Insert content after an element as siblings. after(el)(newSibling)
before Insert content before an element as siblings. before(el)(newSibling)
remove Remove an element from the DOM. remove(modal)
empty Remove all children from an element. empty(listContainer)
wrap Wrap an element with another element. wrap(img)(figure)
clone Deep clone a node. const copy = clone(template);
sanitizeHTMLSimple Sanitize HTML by removing dangerous tags and attributes. const safe = sanitizeHTMLSimple(userInput);
sanitizeHTMLTextOnly Extract text content only from HTML. const text = sanitizeHTMLTextOnly(html);

Event Handling

Function Description Example
on Attach an event listener. Returns a cleanup function. const unsub = on(btn)('click', handler);
onDelegated Attach a delegated listener for dynamic children. onDelegated(list)('li')('click', handler)
dispatch Fire a custom event from an element. dispatch(el)('modal:close', { id: 123 });
createListenerGroup Create a group of listeners with batch cleanup. const group = createListenerGroup();
Evt.stop Stop event propagation. Evt.stop(e);
Evt.prevent Prevent default action. Evt.prevent(e);
Key.isEnter Check if Enter key was pressed. if (Key.isEnter(e)) submit();
Key.isEscape Check if Escape key was pressed. if (Key.isEscape(e)) close();

Forms & Inputs

Function Description Example
Form.serialize Get all form data as an object. const data = Form.serialize(formEl);
Form.populate Fill form fields from an object. Form.populate(formEl)(data);
form Enhanced form wrapper with utilities. const f = form('#myForm');
Input.get Get input value (handles checkboxes, numbers, etc). const val = Input.get(input);
Input.set Set input value. Input.set(input)('value');
Input.watch Watch for input changes. Input.watch(input)(handler);
Input.watchDebounced Watch with debouncing. Input.watchDebounced(input)(handler, 300);
Input.validate Set validation message. Input.validate(input)('Invalid');

State & Attributes

Function Description Example
cls.add Add one or more CSS classes. cls.add(el)('active', 'visible')
cls.remove Remove one or more CSS classes. cls.remove(el)('loading')
cls.toggle Toggle a class, with optional force boolean. cls.toggle(el)('open', isOpen)
cls.has Check if element has a class. if (cls.has(el)('active')) {}
cls.replace Replace one class with another. cls.replace(el)('old', 'new')
watchClass Run callback when a class changes. watchClass(el)('active', handler)
cycleClass Cycle through a list of classes (state machine). cycleClass(el)(['idle', 'loading', 'done'])
Data.set Set a data-* attribute. Data.set(el)('userId', 123)
Data.get Get a data-* attribute as string. const val = Data.get(el)('userId');
Data.read Parse a data-* attribute (auto-detects type). const id = Data.read(el)('userId');
Data.bind Two-way bind data attribute to variable. Data.bind(el)('count', getter, setter)
watchAttr Run callback when attribute changes. watchAttr(el)('disabled', handler)
watchText Run callback when text content changes. watchText(el)(handler)

Lifecycle & Observation

Function Description Example
onReady Run callback when DOM is ready. onReady(() => init());
ready Object with ready state utilities. if (ready.is()) { /* DOM ready */ }
onMount Run callback when element appears in DOM. onMount('.modal')(handler);
waitFor Wait for element to match a predicate. await waitFor(el)(e => e.classList.contains('ready'));

CSS & Styling

Function Description Example
CssVar.get Get CSS variable value. const val = CssVar.get(el)('--color');
CssVar.set Set CSS variable. CssVar.set(el)('--color', 'red');
computed Get computed style property. const color = computed(el)('color');
cssTemplate Create CSS string from template. const css = cssTemplate\color: ${c};`;`
injectStyles Inject CSS into document. Returns cleanup. const unsub = injectStyles('.box{color:red}');
waitTransition Wait for CSS transition to complete. await waitTransition(el);
toColorSpace Convert color to different color space. const rgb = toColorSpace('#fff', 'srgb');

Layout & Geometry

Function Description Example
rect Get DOMRect (position & dimensions). const r = rect(el);
offset Get element offset relative to document. const {top, left} = offset(el);
isVisible Check if element is visible. if (isVisible(el)) {}
scrollInto Scroll element into view with options. scrollInto(el)({ behavior: 'smooth' });
focus Focus an element. focus(input);
blur Blur an element. blur(input);
Focus.trap Trap focus within container. Focus.trap(modal);

Data & Collections

Function Description Example
refs Get all data-ref elements as object. const {btn, input} = refs(root);
groupRefs Get grouped data-ref elements as arrays. const {items} = groupRefs(root);
component Create typed component from refs. const c = component<T>('#root');
store Attach arbitrary data to an element. store(el).set('count', 5);
batch Batch operations on element collection. batch(items).addClass('active');
groupBy Group elements by attribute or callback. groupBy(items)('data-category');
Obj.clone Deep clone an object. const copy = Obj.clone(obj);
Obj.isEqual Deep equality check. if (Obj.isEqual(a, b)) {}
Obj.pick Pick properties from object. const sub = Obj.pick(obj, ['a', 'b']);
Obj.omit Omit properties from object. const rest = Obj.omit(obj, ['x']);

Network & HTTP

Function Description Example
Http.get Simple GET request. const data = await Http.get<T>(url);
Http.post Simple POST request. const res = await Http.post(url)(body)();
Http.put Simple PUT request. const res = await Http.put(url)(body)();
Http.delete Simple DELETE request. const res = await Http.delete(url)({});
Http.create Create configured API client. const api = Http.create({baseURL: '...'});
Async.retry Retry async function with backoff. await Async.retry(fn, {retries: 3});
Async.race Race promises with timeout. await Async.race(promise, 1000);
Async.parallel Run promises in parallel with limit. await Async.parallel(tasks, 5);
createQueue Create async task queue. const q = createQueue({concurrency: 3});

Timing & Async Control

Function Description Example
debounce Delay function until pause in calls. const search = debounce(fn, 300);
throttle Limit function to once per interval. const scroll = throttle(fn, 100);
wait Promise-based delay. await wait(1000);
nextFrame Wait for next animation frame. await nextFrame();
Signal.timeout Create timeout signal for fetch. fetch(url, {signal: Signal.timeout(5000)});
Signal.manual Create manual abort signal. const [signal, abort] = Signal.manual();

Storage & State

Function Description Example
Local.get Get from localStorage (typed). const val = Local.get<T>('key');
Local.set Set to localStorage. Local.set('key', value);
Local.remove Remove from localStorage. Local.remove('key');
Local.watch Watch localStorage key for changes. Local.watch('key')(handler);
Session.get Get from sessionStorage (typed). const val = Session.get<T>('key');
Session.set Set to sessionStorage. Session.set('key', value);
Cookie.get Get cookie value. const val = Cookie.get('name');
Cookie.set Set cookie with options. Cookie.set('name', 'val', {maxAge: 3600});
Cookie.remove Remove cookie. Cookie.remove('name');

URL & Navigation

Function Description Example
Params.get Get URL parameter. const id = Params.get('id');
Params.set Set URL parameter. Params.set('id', '123');
Params.remove Remove URL parameter. Params.remove('id');
Params.getAll Get all URL parameters as object. const params = Params.getAll();
History.push Push new history state. History.push('/page', {data});
History.replace Replace current history state. History.replace('/page', {data});
History.back Go back in history. History.back();
History.forward Go forward in history. History.forward();

Advanced Utilities

Function Description Example
def Create hybrid curried/imperative functions. const fn = def((el, val) => el.value = val);
$ jQuery-like fluent API wrapper. $('.btn').modify({text: 'Hi'}).addClass('active');
$$ Collection wrapper for batch operations. $$('button').forEach(b => modify(b)({disabled: false}));
bind.text Two-way bind text content. bind.text(el, () => count, v => count = v);
bind.value Two-way bind input value. bind.value(input, getter, setter);
bindEvents Bind multiple events at once. bindEvents(el, {click: h1, input: h2});
binder Bind events to refs with schema. binder(refs, schema);
view Create views from HTML strings. const v = view('<div></div>');
apply Apply setters to state. apply(setters)(state);
createBus Create typed event bus (pub/sub). const bus = createBus<Events>();
createStore Create reactive store. const store = createStore({count: 0});
defineComponent Define a component with lifecycle. defineComponent('#app', (ctx) => {...});
mountComponent Mount a component instance. mountComponent(instance, '#root');
Result.ok Create success result. return Result.ok(value);
Result.err Create error result. return Result.err(error);
Option.some Create Some option. return Option.some(value);
Option.none Create None option. return Option.none();
Fn.pipe Pipe value through functions. const result = Fn.pipe(val, fn1, fn2);
Fn.compose Compose functions right-to-left. const fn = Fn.compose(fn3, fn2, fn1);
ViewTransitions.start Start view transition. ViewTransitions.start(() => updateDOM());
SW.register Register service worker. await SW.register('/sw.js');
stripListeners Clone element without event listeners. const clean = stripListeners(el);
instantiate Create instance from template. const inst = instantiate('#template');
cloneMany Clone element multiple times. const copies = cloneMany(el);
cast Type-safe element casting. const btn = cast<'button'>('button');
isElement Check if node is an element. if (isElement(node)) {...}
isTag Check if element matches tag. if (isTag('button')(el)) {...}
isInViewport Check if element is in viewport. if (isInViewport(el)) {...}
animate Animate element with Web Animations API. animate(el).fadeIn();
Text.copy Copy text to clipboard. Text.copy('Hello');
Text.paste Read from clipboard. const text = await Text.paste();

Type Utilities

The library exports TypeScript types and interfaces for better type safety:

Type/Interface Description
ParseSelector<S> Infers element type from CSS selector string.
Unsubscribe Cleanup function type returned by event listeners.
EventMap<T> Event map for HTML elements, extensible for custom events.
ExtractEventDetail<T> Extracts detail type from CustomEvent.
ElementProps Properties for creating/modifying elements.
StrictElementProps<T> Element properties with element-specific validation.
DeepReadonly<T> Makes all properties deeply readonly.
DeepPartial<T> Makes all properties deeply partial.
FormElement Union type for form elements (input, select, textarea).
QueryValue Valid URL query parameter value types.
QueryParams Record of query parameters.
Ok<T> Success result type.
Err<E> Error result type.
Result<T, E> Union of Ok and Err types.
Setter<T> Setter function type.
EventSchema<R> Event schema for binding.
Refs<K> Record of element refs.
HttpMethod HTTP method types.
HttpStatus HTTP status code types.
HttpRequestInit HTTP request configuration.
HttpResponse<T> HTTP response type.
HttpConfig<H> HTTP client configuration.
ComponentContext Component context interface.
ComponentInstance<API> Component instance type.

Advanced Patterns

Example: Form Submission

This shows how the utilities compose to handle a common, real-world scenario.

import { find, on, modify, Form, Http } from '@doeixd/dom';

const form = find<HTMLFormElement>('#login-form');
const submitBtn = find<HTMLButtonElement>('button[type="submit"]', form);

on(form)('submit', async (e) => {
  e.preventDefault();

  // 1. Show loading state
  modify(submitBtn)({ disabled: true, text: 'Logging in...' });

  try {
    // 2. Get form data and make API call
    const data = Form.serialize(form);
    const response = await Http.post('/api/login')(data)();

    // 3. Show success state
    modify(submitBtn)({ text: 'Success!' });
    addClass(form)('form-success');
    
  } catch (error) {
    // 4. Handle errors
    modify(submitBtn)({ disabled: false, text: 'Try Again' });
    addClass(form)('form-error');
  }
});

Organizing with component

For more complex UI, the component helper collects elements with data-ref attributes into a typed object, avoiding repeated find calls.

HTML:

<div id="profile-card">
  <h2 data-ref="name"></h2>
  <p data-ref="email"></p>
  <button data-ref="editBtn">Edit</button>
</div>

TypeScript:

import { component, on, modify } from '@doeixd/dom';

const profile = component<{
  name: HTMLHeadingElement;
  email: HTMLParagraphElement;
  editBtn: HTMLButtonElement;
}>('#profile-card');

// Now access elements directly and with full type safety
modify(profile.name)({ text: 'Jane Doe' });
on(profile.editBtn)('click', () => openEditModal());

Advanced Features

The library includes powerful features for building modern, reactive UIs without a framework.

h/tags - Hyperscript Element Creation

VanJS-style element creation using Proxy-based property access. Cleaner syntax than el() with automatic SVG namespace handling.

import { h, refs } from '@doeixd/dom';

// Create elements with clean syntax
const card = h.div({ class: { card: true } }, [
  h.h2({ dataRef: 'title' }, ['Card Title']),
  h.p({ dataRef: 'content' }, ['Card description']),
  h.button({ dataRef: 'btn' }, ['Action'])
]);

// SVG elements get correct namespace automatically
const icon = h.svg({ attr: { viewBox: '0 0 24 24', width: '24', height: '24' } }, [
  h.path({ attr: { d: 'M12 2L2 12h3v8h5v-6h4v6h5v-8h3z', fill: 'currentColor' } })
]);

// Extract refs for type-safe access
const { title, content, btn } = refs(card);
title.textContent = 'Updated Title';

When to use:

  • Building declarative UI hierarchies
  • Creating SVG graphics (automatic namespace)
  • Component templates with viewRefs()

đź“– Full Documentation

viewRefs() - Typed Component Templates

Create reusable component templates with automatic ref extraction and smart update methods.

import { viewRefs, h } from '@doeixd/dom';

// Define typed template
interface CardRefs {
  title: HTMLElement;
  content: HTMLElement;
  button: HTMLElement;
}

const Card = viewRefs<CardRefs>(({ refs }) =>
  h.div({ class: { card: true } }, [
    h.h2({ dataRef: 'title' }),
    h.p({ dataRef: 'content' }),
    h.button({ dataRef: 'button' }, ['Click'])
  ])
);

// Create instances with full type safety
const { element, refs, updateRefs, bind } = Card({
  className: 'featured-card'
});

// Smart updates handle strings, numbers, and ElementProps
updateRefs({
  title: 'Card Title',
  content: 'Description text',
  button: { text: 'Buy Now', class: { primary: true } }
});

// Get setter functions for individual refs
const setTitle = bind('title');
setTitle('Updated Title');

document.body.appendChild(element);

When to use:

  • Reusable component patterns
  • Type-safe ref access
  • Dynamic UI that needs frequent updates

đź“– Full Documentation

List() - Reactive Array Binding

Efficient DOM rendering for dynamic collections with three reconciliation strategies.

import { List, h } from '@doeixd/dom';

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

const container = document.querySelector('#todos');

// Keyed mode for efficient updates
const list = List<Todo>(container, {
  key: todo => todo.id,  // Enables smart diffing
  render: (todo) => h.li({
    class: { done: todo.done }
  }, [
    h.input({ attr: { type: 'checkbox', checked: todo.done } }),
    h.span({}, [todo.text])
  ]),
  update: (el, todo) => {
    // Optional: efficient update without re-rendering
    el.classList.toggle('done', todo.done);
  }
});

// Rich API for list manipulation
list.set([
  { id: 1, text: 'Buy groceries', done: false },
  { id: 2, text: 'Walk dog', done: true }
]);

list.append([{ id: 3, text: 'Call mom', done: false }]);
list.remove(todo => todo.done);  // Remove completed items

Three modes:

  • Default: Simple blow-away (fast for small lists)
  • Keyed: Efficient diffing with key function (reuses elements)
  • Custom: User-provided reconciliation (e.g., morphdom)

đź“– Full Documentation

createBinder() - Type-Safe Data Binding

Schema-based binding between data and DOM with automatic dirty checking.

import { createBinder, bind, refs, h } from '@doeixd/dom';

const form = h.form({}, [
  h.input({ dataRef: 'nameInput', attr: { type: 'text' } }),
  h.input({ dataRef: 'emailInput', attr: { type: 'email' } }),
  h.button({ dataRef: 'submitBtn' }, ['Submit']),
  h.div({ dataRef: 'errorMsg', class: { error: true } })
]);

const formRefs = refs(form);

// Create binder with schema
const ui = createBinder(formRefs, {
  nameInput: bind.value,
  emailInput: bind.value,
  submitBtn: (el) => bind.prop('disabled', el),
  errorMsg: bind.text
});

// Update UI declaratively
ui({
  nameInput: 'John Doe',
  emailInput: 'john@example.com',
  submitBtn: false,
  errorMsg: ''
});

// Individual setters for event handlers
ui.set.errorMsg('Invalid email address');

// Batch updates
ui.batch(() => {
  ui({ submitBtn: true });
  ui({ errorMsg: 'Submitting...' });
});

Bind primitives:

  • bind.text - Text content
  • bind.html - Inner HTML
  • bind.value - Input values (with dirty checking)
  • bind.prop() - Element properties (disabled, checked, etc.)
  • bind.classes - Multiple class toggles
  • bind.show - Show/hide elements
  • bind.attr() - HTML attributes
  • bind.toggle() - Single class toggle

đź“– Full Documentation

Complete Example: Todo App

Combining all features for a complete reactive UI:

import { h, viewRefs, List, createBinder, bind, refs } from '@doeixd/dom';

// 1. Define component template
interface TodoRefs {
  checkbox: HTMLElement;
  text: HTMLElement;
  deleteBtn: HTMLElement;
}

const TodoItem = viewRefs<TodoRefs>(({ refs }) =>
  h.li({ class: { 'todo-item': true } }, [
    h.input({ dataRef: 'checkbox', attr: { type: 'checkbox' } }),
    h.span({ dataRef: 'text' }),
    h.button({ dataRef: 'deleteBtn' }, ['Ă—'])
  ])
);

// 2. Create form with binder
const form = h.div({ class: { 'todo-app': true } }, [
  h.input({ dataRef: 'input', attr: { type: 'text', placeholder: 'Add todo...' } }),
  h.button({ dataRef: 'addBtn' }, ['Add']),
  h.ul({ dataRef: 'list' })
]);

const { input, addBtn, list } = refs(form);

const formUI = createBinder({ input, addBtn }, {
  input: bind.value,
  addBtn: (el) => bind.prop('disabled', el)
});

// 3. Create reactive list
interface Todo {
  id: number;
  text: string;
  done: boolean;
}

let todos: Todo[] = [];

const todoList = List<Todo>(list, {
  key: todo => todo.id,
  render: (todo) => {
    const { element, refs } = TodoItem();
    const ui = createBinder(refs, {
      checkbox: (el) => bind.prop('checked', el),
      text: bind.text
    });

    ui({ checkbox: todo.done, text: todo.text });

    refs.checkbox.addEventListener('change', () => {
      todo.done = (refs.checkbox as HTMLInputElement).checked;
      todos = [...todos];  // Trigger re-render
      todoList.set(todos);
    });

    refs.deleteBtn.onclick = () => {
      todos = todos.filter(t => t.id !== todo.id);
      todoList.set(todos);
    };

    return element;
  }
});

// 4. Add todo handler
addBtn?.addEventListener('click', () => {
  const text = (input as HTMLInputElement)?.value.trim();
  if (text) {
    todos.push({ id: Date.now(), text, done: false });
    todoList.set(todos);
    formUI({ input: '' });
  }
});

document.body.appendChild(form);

Troubleshooting & Common Patterns

Gotchas & Things to Know

1. Null Safety is Built-in All functions handle null/undefined gracefully. You don't need to check if elements exist:

// This is safe - no errors even if button doesn't exist
const btn = find(document)('.missing-button'); // null
modify(btn)({ text: 'Hello' }); // Does nothing, returns null
on(btn)('click', handler); // Returns no-op cleanup function

2. Curried Functions Support Two Styles

Most functions work in both imperative and curried style:

// Imperative (all args at once)
modify(element, { text: 'Hello' });
css(element, { color: 'red' });

// Curried (target first, config later)
modify(element)({ text: 'Hello' });
css(element)({ color: 'red' });

// Curried style enables composition
const updateButton = modify(button);
updateButton({ text: 'Loading...' });
updateButton({ text: 'Done!' });

3. Class Objects Use Boolean Toggles

When using the class property in modify, use boolean values to toggle classes:

// Correct âś“
modify(el)({
  class: {
    active: true,      // Add 'active'
    disabled: false,   // Remove 'disabled'
    loading: isLoading // Conditional
  }
});

// Wrong âś—
modify(el)({ class: 'active disabled' }); // This won't work

4. Dataset Auto-Converts to Kebab-Case

Data attributes are automatically converted:

modify(el)({
  dataset: {
    userId: 123,       // becomes data-user-id="123"
    isActive: true,    // becomes data-is-active="true"
    apiEndpoint: '/api' // becomes data-api-endpoint="/api"
  }
});

5. Event Listeners Return Cleanup Functions

Always store and call cleanup functions to prevent memory leaks:

// Store the cleanup
const unsub = on(button)('click', handler);

// Later, when component unmounts or modal closes
unsub(); // Removes the listener

// For multiple listeners
const cleanups = [
  on(btn1)('click', h1),
  on(btn2)('click', h2),
  on(input)('input', h3)
];

// Clean up all at once
cleanups.forEach(fn => fn());

6. Type Inference Works Best with Literals

Use string literals (not variables) for selectors to get automatic type inference:

// Good: Type is inferred as HTMLButtonElement | null
const btn = find(document)('button');

// Less ideal: Type is generic HTMLElement | null
const selector = 'button';
const btn2 = find(document)(selector);

// Solution: Use type annotation
const btn3 = find(document)<'button'>(selector);
// Or: Type assertion
const btn4 = find(document)(selector) as HTMLButtonElement | null;

7. onDelegated Has Different Signature

Event delegation requires selector first, then event:

// Regular event listener
on(button)('click', handler);

// Delegated listener (note the extra selector call)
onDelegated(container)('button')('click', handler);
//                     ^^^^^^^^ selector for matching children

8. HTML Templates Don't Auto-Escape

The html function uses innerHTML, so be careful with user input:

// Unsafe with user input âś—
const userInput = getUserInput();
const div = html`<div>${userInput}</div>`; // XSS risk!

// Safe alternatives âś“
const div = el('div')({ text: userInput }); // Uses innerText
modify(div)({ text: userInput }); // Uses innerText

Common Patterns & Idioms

Pattern 1: Conditional Element Updates

// Update multiple elements based on state
const updateUI = (state: AppState) => {
  modify(submitBtn)({
    text: state.loading ? 'Loading...' : 'Submit',
    disabled: state.loading,
    class: { loading: state.loading, error: state.error }
  });

  if (state.error) {
    modify(errorMsg)({ text: state.error, style: { display: 'block' } });
  } else {
    modify(errorMsg)({ style: { display: 'none' } });
  }
};

Pattern 2: Batch Operations on Collections

// Apply same operation to multiple elements
const buttons = findAll(document)('button');

// Imperative style
buttons.forEach(btn => modify(btn)({ disabled: true }));

// Or use the $$ wrapper
$$(buttons).forEach(btn => {
  modify(btn)({ disabled: true });
  cls.add(btn)('disabled-state');
});

Pattern 3: Cleanup on Component Unmount

class MyComponent {
  private cleanups: Unsubscribe[] = [];

  mount(root: HTMLElement) {
    const btn = find(root)('button');

    // Store all cleanup functions
    this.cleanups.push(
      on(btn)('click', this.handleClick),
      on(window)('resize', this.handleResize),
      Local.watch('user')(this.handleUserChange)
    );
  }

  unmount() {
    // Clean up all listeners at once
    this.cleanups.forEach(fn => fn());
    this.cleanups = [];
  }
}

Pattern 4: Loading States with tempStyle

async function fetchData() {
  const btn = find(document)('button');

  // Apply temporary loading styles
  const revert = tempStyle(btn)({
    opacity: '0.6',
    pointerEvents: 'none',
    cursor: 'wait'
  });

  try {
    const data = await Http.get('/api/data');
    return data;
  } finally {
    revert(); // Always restore original styles
  }
}

Pattern 5: Form Validation with Input Watchers

const emailInput = find<HTMLInputElement>(document)('#email');

Input.watchDebounced(emailInput)(async (value) => {
  // Don't validate if empty
  if (!value) {
    Input.validate(emailInput)('');
    return;
  }

  // Async validation
  const isValid = await checkEmailAvailable(value);
  Input.validate(emailInput)(
    isValid ? '' : 'Email already taken'
  );
}, 300);

Pattern 6: Modal with Focus Trap and Cleanup

function openModal() {
  const modal = find(document)('.modal');
  const closeBtn = find(modal)('.close-btn');

  modify(modal)({ style: { display: 'block' } });
  cls.add(document.body)('modal-open');

  // Trap focus within modal
  const releaseFocus = Focus.trap(modal);

  // Close on button or escape
  const cleanups = [
    releaseFocus,
    on(closeBtn)('click', closeModal),
    on(document)('keydown', (e) => {
      if (Key.isEscape(e)) closeModal();
    })
  ];

  function closeModal() {
    modify(modal)({ style: { display: 'none' } });
    cls.remove(document.body)('modal-open');
    cleanups.forEach(fn => fn());
  }
}

Pattern 7: Optimistic UI Updates

async function deleteItem(itemId: string) {
  const item = find(document)(`[data-id="${itemId}"]`);

  // Optimistic: Remove from UI immediately
  const parent = item?.parentElement;
  const nextSibling = item?.nextSibling;
  remove(item);

  try {
    await Http.delete(`/api/items/${itemId}`)({});
  } catch (error) {
    // Rollback: Restore item on error
    if (parent && item) {
      if (nextSibling) {
        before(nextSibling)(item);
      } else {
        append(parent)(item);
      }
    }
    alert('Failed to delete item');
  }
}

Pattern 8: Infinite Scroll with Intersection Observer

const container = find(document)('.infinite-scroll');
const sentinel = find(container)('.sentinel');

let page = 1;
const observer = new IntersectionObserver(async (entries) => {
  if (entries[0].isIntersecting) {
    const items = await fetchPage(page++);
    const elements = items.map(item =>
      el('div')({ class: { item: true } })([item.name])
    );
    append(container)(...elements);
  }
});

if (sentinel) observer.observe(sentinel);

Pattern 9: Component Pattern with Refs

interface ProfileCardRefs {
  name: HTMLHeadingElement;
  email: HTMLParagraphElement;
  avatar: HTMLImageElement;
  editBtn: HTMLButtonElement;
}

function createProfileCard(userData: User) {
  const card = component<ProfileCardRefs>('#profile-card');

  // Type-safe access to all refs
  modify(card.name)({ text: userData.name });
  modify(card.email)({ text: userData.email });
  modify(card.avatar)({ attr: { src: userData.avatarUrl } });

  return on(card.editBtn)('click', () => editProfile(userData.id));
}

Pattern 10: State Machine with cycleClass

const button = find(document)('.toggle-btn');

// Define state classes
const states = ['idle', 'loading', 'success', 'error'];
const nextState = cycleClass(button)(states);

// Initial state
cls.add(button)('idle');

async function handleAction() {
  nextState(); // idle -> loading

  try {
    await performAction();
    nextState(); // loading -> success

    setTimeout(() => {
      nextState(); // success -> error
      nextState(); // error -> idle (cycles back)
    }, 2000);
  } catch (err) {
    nextState(); // loading -> success
    nextState(); // success -> error
  }
}

License

MIT