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.
Installation
Via npm:
npm install exdom
When not using a bundler, exdom can be imported via CDN:
export * from 'https://cdn.jsdelivr.net/npm/exdom@2.0.0/dist/exdom.min.js';
declare module 'https://cdn.jsdelivr.net/npm/exdom@2.0.0/dist/exdom.min.js' {
export * from 'exdom';
}
import { ... } from './exdom.js';
Usage
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';
interface TodoAppEvents {
addTodoItem: string;
toggleTodoItem: string;
todoItem: TodoItemData;
}
interface TodoItemData {
id: string;
label: string;
done: boolean;
}
function TodoApp(el: CustomEventElement<TodoAppEvents>) {
el.innerHTML = `
<h1>To-Do</h1>
<p>
<input type="text" name="label">
<button type="button" class="add">Add</button>
</p>
<ul class="items"></ul>
`;
let items: TodoItemData[] = [
{
id: crypto.randomUUID(),
label: 'Hello, world!',
done: false,
},
];
function update() {
reconcile({
container: qsr(el, '.items'),
items,
create: () => TodoItem(document.createElement('li')),
update: (el, item) =>
el.dispatchEvent(new TypedCustomEvent('todoItem', { detail: item })),
});
}
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);
});
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();
return el;
}
function TodoItem(el: CustomEventElement<TodoAppEvents>) {
el.innerHTML = `
<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;
}
TodoApp(qsr(document, '#app'));
See also API reference
Tests
npx playwright install
(once)npm test