Virtual DOM (vnode)
A lightweight, Snabbdom-style virtual DOM. It represents your UI as plain JavaScript objects (VNodes), diffs an old tree against a new one, and applies only the differences to the real DOM.
init()builds the reconciler and returns apatchfunction. The bundled modules (class / props / attrs / style / events) are registered automatically.patch(oldVnode, newVnode)mounts a tree (whenoldVnodeis a real DOM element) or diffs two vnode trees and updates the DOM in place.h(sel, dataOrChildren?, children?)is the hyperscript helper that buildsVNodes.
Import
import { init, h, classModule, propsModule, styleModule, eventListenersModule } from 'ranuts/vnode';Note: in this implementation
init()takes no arguments — the module set is fixed and registered internally, so the individual*Moduleexports do not need to be passed toinit. They are exported for reference and inspection.
Example
Quick start
import { init, h } from 'ranuts/vnode';
// init() returns a `patch` function.
// The bundled modules (class, props, attrs, style, events) are registered automatically.
const patch = init();
const container = document.getElementById('app');
// Build a vnode tree
let vnode = h('div#app.container', { style: { color: 'red' } }, [
h('h1', 'Hello vnode'),
h('button', { on: { click: () => console.log('clicked') } }, 'Click me'),
]);
// Initial render: pass a real DOM element as the old vnode to mount into it
patch(container, vnode);
// Later: build an updated tree and patch the previous vnode into it.
// Only the differences (text, style, listeners) are applied to the DOM.
const newVnode = h('div#app.container', { style: { color: 'green' } }, [
h('h1', 'Hello again'),
h('button', { on: { click: () => console.log('clicked') } }, 'Updated'),
]);
patch(vnode, newVnode);
vnode = newVnode; // keep the latest tree for the next patchBuilding nodes with h
// tag only
h('div');
// tag + data
h('div', { class: { active: true } });
// tag + a single text child
h('span', 'hello');
// tag + children array
h('ul', [h('li', 'one'), h('li', 'two')]);
// tag + data + children
h('a', { attrs: { href: '/home' } }, 'Home');
// CSS-style selectors set id and classes
h('div#main.card.large', 'content'); // <div id="main" class="card large">content</div>
// SVG namespaces are applied automatically when the selector starts with "svg"
h('svg', { attrs: { width: 100, height: 100 } }, [h('circle', { attrs: { cx: 50, cy: 50, r: 40 } })]);API
init()
Creates the reconciler and returns a patch function. The bundled modules are registered internally; it takes no arguments.
Return
| Value | Description | Type |
|---|---|---|
patch | Mounts / diffs vnode trees against the real DOM | (oldVnode: VNode | Element, vnode: VNode) => VNode |
patch(oldVnode, vnode)
Returned by init(). On the first call, pass a real DOM Element as oldVnode to mount the tree into it. On later calls, pass the previous VNode to diff-and-update in place. Returns the new VNode, which you keep as the "old" value for the next call.
Parameters
| Parameter | Description | Type |
|---|---|---|
oldVnode | The previous vnode, or a DOM element on first mount | VNode | Element |
vnode | The new vnode tree to render | VNode |
h(sel, dataOrChildren?, children?)
Hyperscript helper that builds a VNode. It is overloaded:
| Signature | Description |
|---|---|
h(sel) | Element from a selector only |
h(sel, data) | Element with VNodeData (data may be null) |
h(sel, children) | Element with children — a text/number, a VNode, or an array |
h(sel, data, children) | Element with data and children |
Parameters
| Parameter | Description | Type |
|---|---|---|
sel | CSS-style selector: tag, tag#id, tag.class, combined (div#id.a.b). svg… auto-adds the SVG namespace | string |
data | Node data — class / props / attrs / style / listeners / key / hook. May be null | VNodeData | null |
children | A text or number (becomes a text node), a single VNode, or an array of them | VNodeChildren |
VNodeData fields
| Field | Description | Type | Applied by |
|---|---|---|---|
props | DOM properties set via elm[key] = value | Record<string, any> | propsModule |
attrs | HTML attributes set via setAttribute (true/false toggle the attr) | Record<string, string | number | boolean> | attributesModule |
class | Conditional classes — a name → boolean map | Record<string, boolean> | classModule |
style | Inline styles — a name → value map (--var keys use CSS variables) | Record<string, any> | styleModule |
on | Event listeners — event → handler (or an array of handlers) | Record<string, Function | Function[]> | eventListenersModule |
key | Stable identity used by the diff algorithm to match/reorder children | string | number | (diff core) |
ns | Namespace URI (set automatically for SVG subtrees) | string | (diff core) |
hook | Per-vnode lifecycle hooks (Hooks) | Hooks | (type surface — see note) |
Note:
hookand theHookstype are part of the public type surface. This trimmed implementation drives the DOM through the module lifecycle (create/update/destroy); per-vnodedata.hookcallbacks are not invoked by the currentpatchloop.
Modules
Each module handles one slice of VNodeData. init() registers all of them; they are also exported individually.
| Export | Handles | Description |
|---|---|---|
classModule | data.class | Adds/removes classes from a name → boolean map |
propsModule | data.props | Assigns DOM properties directly (elm[key] = value) |
attributesModule | data.attrs | Sets/removes HTML attributes via setAttribute (incl. xml/xlink) |
styleModule | data.style | Sets inline styles and CSS custom properties |
eventListenersModule | data.on | Attaches/detaches event listeners |
modules | — | The default registry object mapping each module name to its module |
Lower-level exports
| Export | Type | Description |
|---|---|---|
vnode | (sel, data, children, text, elm) => VNode | Low-level VNode factory used internally by h. Prefer h in application code. |
addNS | (data, children, sel) => void | Recursively applies the SVG namespace to a subtree. Called automatically by h for svg… selectors. |
htmlDomApi | DOMAPI | The default browser DOM adapter used internally by patch (create/insert/remove/text nodes, etc.). |
is | { array, isStr, primitive, isVnode } | Small type-guard helpers used across the vnode internals. |
Chain | class Chain | A chainable imperative DOM builder (setAttribute, append, setTextContent, …). Independent of the vnode diff. |
create | (tagName, options?) => Chain | Convenience factory returning a new Chain. |
Types
| Type | Shape / meaning |
|---|---|
VNode | { sel, data, children, elm, text, key, listener? } — a virtual node |
VNodeData | { props?, attrs?, class?, style?, on?, key?, ns?, hook? } — see fields above |
VNodes | VNode[] |
VNodeChildElement | VNode | string | number |
VNodeChildren | VNodeChildElement | VNodeChildElement[] |
ArrayOrElement<T> | T | T[] |
Key | string | number |
Hooks | { pre?, init?, create?, insert?, prepatch?, update?, postpatch?, destroy?, remove?, post? } |
DOMAPI | Interface describing the DOM operations patch uses (see htmlDomApi) |
Fragment | DocumentFragment extension used for fragment handling |
Modules | Record<string, Record<string, ModuleHook>> — the module registry shape |
ModuleHook | A single module lifecycle callback |
Notes
- Browser only.
ranuts/vnodetouchesdocumentand DOM APIs; import it in browser code, not in Node. - Keep the last vnode.
patchreturns the newVNode. Store it and pass it asoldVnodeon the next update so diffs are computed against the current tree. textandchildrenare mutually exclusive on aVNode— a node is either a text node or an element with children.- Use
keyfor lists. When rendering dynamic lists, give siblings stablekeyvalues so the diff can match and reorder nodes instead of recreating them.