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.
npm install @doeixd/domUse 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><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>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!');
});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');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.
This function creates a self-contained logic unit attached to a DOM element. It solves three major problems in Vanilla JS development:
- Ref Management: Automatically maps
data-refelements to variables. - Lifecycle Management: Automatically cleans up event listeners and observers when the component is destroyed.
- State Sync: Provides a simple way to sync JavaScript state with DOM data attributes.
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 listenersThe 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. |
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
| 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); |
| 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); |
| 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(); |
| 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'); |
| 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) |
| 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')); |
| 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'); |
| 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); |
| 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']); |
| 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}); |
| 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(); |
| 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'); |
| 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(); |
| 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(); |
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. |
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');
}
});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());The library includes powerful features for building modern, reactive UIs without a framework.
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
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
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 itemsThree 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
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 contentbind.html- Inner HTMLbind.value- Input values (with dirty checking)bind.prop()- Element properties (disabled, checked, etc.)bind.classes- Multiple class togglesbind.show- Show/hide elementsbind.attr()- HTML attributesbind.toggle()- Single class toggle
đź“– Full Documentation
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);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 function2. 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 work4. 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 children8. 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 innerTextPattern 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
}
}MIT