exdom

exdom

Essential DOM utilities.

  • Query selector shortcuts
  • Soft HTML, text, attribute, and input value setters
  • Reconciliation
  • Type-safe DOM event management

Inspired by learnings from VANILLA TODO, a case study on viable techniques for vanilla web development.

Via npm:

npm install exdom

When not using a bundler, exdom can be imported via CDN:

// src/exdom.js or src/exdom.ts
export * from 'https://cdn.jsdelivr.net/npm/exdom@2.0.0/dist/exdom.min.js';

// src/exdom.d.ts
declare module 'https://cdn.jsdelivr.net/npm/exdom@2.0.0/dist/exdom.min.js' {
export * from 'exdom';
}

// src/app.js or src/app.ts
import { ... } from './exdom.js';

// ...

A very basic to-do app with exdom could look like this (in TypeScript, but JavaScript is equally possible) and covers most of exdom's functions:

import {
CustomEventElement,
qsr,
reconcile,
requestAnimationFrameOnce,
setText,
setValue,
TypedCustomEvent,
} from 'exdom';

// Type-safe custom DOM event details

interface TodoAppEvents {
addTodoItem: string;
toggleTodoItem: string;
todoItem: TodoItemData;
}

interface TodoItemData {
id: string;
label: string;
done: boolean;
}

// Component functions

function TodoApp(el: CustomEventElement<TodoAppEvents>) {
// Base HTML

el.innerHTML = /* html */ `
<h1>To-Do</h1>
<p>
<input type="text" name="label">
<button type="button" class="add">Add</button>
</p>
<ul class="items"></ul>
`;

// Initial data

let items: TodoItemData[] = [
{
id: crypto.randomUUID(),
label: 'Hello, world!',
done: false,
},
];

// Idempotent update

function update() {
// Reconcile todo item components with item list
reconcile({
container: qsr(el, '.items'),
items,
create: () => TodoItem(document.createElement('li')),
update: (el, item) =>
el.dispatchEvent(new TypedCustomEvent('todoItem', { detail: item })),
});
}

// Action handlers

el.addEventListener('addTodoItem', (e) => {
const newItem = {
id: crypto.randomUUID(),
label: e.detail,
done: false,
};

items = [...items, newItem];

requestAnimationFrameOnce(update);
});

el.addEventListener('toggleTodoItem', (e) => {
items = items.map((item) =>
item.id === e.detail ? { ...item, done: !item.done } : item,
);

requestAnimationFrameOnce(update);
});

// UI Events

const labelInput = qsr<HTMLInputElement>(el, '[name=label]');

qsr(el, '.add').addEventListener('click', () => {
el.dispatchEvent(
new TypedCustomEvent('addTodoItem', {
detail: labelInput.value,
bubbles: true,
}),
);
setValue(labelInput, '');
});

update(); // Initial update

return el;
}

function TodoItem(el: CustomEventElement<TodoAppEvents>) {
el.innerHTML = /* html */ `
<input type="checkbox">
<label></label>
`;

let data: TodoItemData;

function update() {
setText(qsr(el, 'label'), data.label);
setValue(qsr(el, '[type=checkbox]'), data.done);
}

el.addEventListener('todoItem', (e) => {
data = e.detail;
requestAnimationFrameOnce(update);
});

qsr(el, '[type=checkbox]').addEventListener('click', () => {
el.dispatchEvent(
new TypedCustomEvent('toggleTodoItem', {
detail: data.id,
bubbles: true,
}),
);
});

return el;
}

// Mount to document

TodoApp(qsr(document, '#app'));

See also API reference

  • npx playwright install (once)
  • npm test