@ludeschersoftware/dom
v1.1.2
Published
A lightweight, highly composable DOM factory that lets you build HTML elements with nested structure, inline styles, dataset attributes, event bindings, and fully typed ref support
Readme
A lightweight, fully-typed DOM helper library for creating and appending HTML elements with a declarative, recursive API. Built in TypeScript, it provides:
- Type-safe tag names and element types
- Strongly-typed props, events, styles, datasets, and arbitrary attributes
- Recursive child definitions including strings, existing nodes, and nested configs
- Helpers for single or batch creation/appending
Installation
npm install @ludeschersoftware/dom
# or
yarn add @ludeschersoftware/domQuick Start
import {
createElement,
appendElement,
createElements,
appendElements,
} from "@ludeschersoftware/dom";
// 1) Create a <button> with text, style, click handler
const btn = createElement("button", {
textContent: "Click Me",
style: { backgroundColor: "navy", color: "white", padding: "0.5em 1em" },
onclick: () => alert("Hello!"),
});
document.body.appendChild(btn);
// 2) Append a list of <li> into a <ul>
const items = ["Apple", "Banana", "Cherry"];
appendElement(
document.body,
"ul",
{},
createElements(
items.map((item) => ({
tagName: "li",
options: { textContent: item },
children: [],
}))
)
);API Reference
| Function | Signature | Return | Notes |
| -------------- | -------------------------------------------------------------------- | ------------------- | --------------------------------------------------- |
| createElement | createElement<T extends TTagName>(tagName: T, options?, children?) | TElementOf<T> | Core builder, returns a typed HTML element |
| createElements | createElements<Cfgs extends TRecursiveElementObject[]>(data: Cfgs) | Typed element array | Builds multiple elements at once |
| appendElement | appendElement(parent, tagName, options?, children?) | void | Shorthand: parent.appendChild(createElement(...)) |
| appendElements | appendElements(parent, data: TRecursiveElementObject[]) | void | Shorthand for batching multiple appendChild calls |
Core Types
type TTagName = keyof HTMLElementTagNameMap;
type TElementOf<T extends TTagName> = HTMLElementTagNameMap[T];
interface TRecursiveElementObject<T extends TTagName = TTagName> {
readonly tagName: T;
readonly options?: TElementOptions<TElementOf<T>>;
readonly children?: TElementChildren;
}
type TElementChild = TRecursiveElementObject | HTMLElement | Node | string;
type TElementChildren = readonly TElementChild[];
interface TElementOptions<T extends HTMLElement> {
attributes?: Record<string, string>; // arbitrary HTML attributes
style?: Partial<TWritableCSSProperties>; // CSS-in-JS style object
dataset?: Record<string, string>; // data-* attributes
ref?: TRef<T>; // callback or mutable ref object
// … plus any partial HTMLElement props and on<Event> handlers
}Feature Highlights
1. Type-Safe Tag Names
Every call to createElement("div", …) returns a HTMLDivElement, not just HTMLElement.
IDE autocomplete guides you to properties and events specific to that element.
2. Declarative Children
Children can be:
- Plain strings (converted to
Textnodes) - Existing
HTMLElementor anyNode(passed through) - Nested configs (
TRecursiveElementObject) for deep trees
// Mixed children: text, element, nested config
const p = createElement("p", {}, [
"Hello, ",
document.createElement("strong"),
{ tagName: "em", options: { textContent: "world" }, children: [] },
]);3. Props, Events & Attributes
createElement("input", {
type: "checkbox",
checked: true, // native prop
onchange: (e) => console.log(e.target), // event handler
attributes: { id: "agree", "aria-label": "Agree" }, // arbitrary attrs
dataset: { toggle: "yes" }, // data-toggle="yes"
});4. Styles & Dataset
createElement("div", {
style: { display: "flex", gap: "1em", padding: "1em" },
dataset: { userId: "42", role: "admin" },
});5. Refs
const refObj = { current: null as HTMLButtonElement | null };
const refFn = (el: HTMLButtonElement) => console.log("got", el);
createElement("button", { ref: refObj, onclick: () => {} }, ["OK"]);
createElement("button", { ref: refFn }, ["Cancel"]);
// later
console.log(refObj.current); // the actual <button> element6. Batch Creation & Appending
// create an array of <li> elements
const lis = createElements([
{ tagName: "li", options: { textContent: "One" }, children: [] },
{ tagName: "li", options: { textContent: "Two" }, children: [] },
]);
// append them under a <ul>
appendElement(document.body, "ul", {}, lis);Advanced Example: Nested Component Tree
appendElements(document.body, [
{
tagName: "section",
options: { id: "main" },
children: [
{
tagName: "h1",
options: { textContent: "Dashboard" },
children: [],
},
{
tagName: "div",
options: { className: "cards", style: { display: "grid", gap: "1em" } },
children: [
{
tagName: "article",
options: { className: "card" },
children: [
{
tagName: "h2",
options: { textContent: "Card 1" },
children: [],
},
{
tagName: "p",
options: { textContent: "Details about card 1" },
children: [],
},
],
},
{
tagName: "article",
options: { className: "card" },
children: [
{
tagName: "h2",
options: { textContent: "Card 2" },
children: [],
},
{
tagName: "p",
options: { textContent: "Details about card 2" },
children: [],
},
],
},
],
},
],
},
]);TypeScript Tips
- If you ever need to pass raw
TextorComment, just include the node directly in thechildrenarray. - Use
as conston deeply nested literal configs to preserve literaltagNametypes. - Extend the global
HTMLElementTagNameMapto teach TS about your custom elements:declare global { interface HTMLElementTagNameMap { "my-widget": MyWidgetElement; } }
Contributing
- Fork the repo
- Create a feature branch
- Add tests under
tests/ - Submit a PR
License
MIT © Johannes Ludescher
