REFERENCE / CORE API
html
A standard tagged template literal. The template is parsed once per call site, turned into a <template>, and every instance is cloneNode(true) plus one tiny effect per dynamic hole. Every hole's DOM position is resolved to a numeric path up front — no virtual DOM, nothing is diffed.
Signature
html(strings: TemplateStringsArray, ...values: unknown[]): Node
Returns
A Node: the element itself when the template has a single root, a DocumentFragment when it has several. Returned by a component function, handed to mount(), or placed in another template's child hole.
The hole rule
A hole is exactly one of three things, and it means the same thing whether the file is compiled or not:
html`<p>${'written once'}</p>` // a constant — written at creation, never again
html`<p>${count}</p>` // a signal — one text node follows it
html`<p>${() => a.value * b.value}</p>` // a function — wrapped in an effect
Each hole gets its own effect. When one signal changes, exactly one expression re-evaluates and exactly one node is touched. Writing a signal with the value it already had touches nothing — not a cheap diff, zero work.
Attribute and event holes
html`<input class="${cls}" value="${text}" oninput=${(e) => (text.value = e.target.value)} />`
// class="${x}" — a hole must be the WHOLE attribute value; class="a ${x}" is a build error
// value="${text}" — value, checked, selected, indeterminate are written as PROPERTIES, so form state tracks the signal
// oninput=${fn} — on* holes attach a real listener on this element; a non-function throws
The reserved ref attribute
ref is the one attribute with built-in meaning: it hands you the element, once, and is never written to the DOM.
let input;
html`<input ref=${(el) => (input = el)} />`; // called once with the element; never written to the DOM
html`<input ref=${inputSignal} />`; // or a signal: assigned the element
Child holes
A child hole accepts text, a node, an array of nodes, or a function returning any of those — and switches shape freely as the function's result changes.
html`<ul>${items.value.map((it) => html`<li>${it}</li>`)}</ul>` // an array of nodes
html`<div>${() => (open.value ? Panel() : '')}</div>` // a conditional: a node, or nothing
html`<ul>${each(items, (it) => it.id, Item)}</ul>` // a keyed list — see each()
// an array hole holds NODES: html`<p>${['a', 'b']}</p>` throws; wrap text in a template
A strict subset of HTML
The parser refuses to guess, so the errors are specific and arrive at build time (and in the editor, with the extension). What it rejects, and the cure in each message:
html`<div><p>hi</div>` // build error: unexpected </div> — closing tags are explicit
html`<div/>` // build error: self-closing is for void elements (and inside <svg>)
html`<circle r=${r}/>` // build error: quote it, or leave a space — the browser mis-parses this
html`<textarea>${text}</textarea>` // build error: bind value="${text}" instead — raw-text content is static
html`<table><tr>…</tr></table>` // build error: wrap rows in <tbody> — the browser would insert one and shift every path
Inside <svg> the rules are SVG's: /> self-closes any element and attribute names keep their case.
Compiled, it disappears
amo build replaces each html`` call with a hoisted template and direct node walks, and the parser is not shipped at all. Semantics are identical by test: every fixture in the repository runs in both modes.
Try it
import { signal, html, mount } from '@amojs.dev/core';
const name = signal('world');
const loud = signal(false);
// the three kinds of hole: a signal (bound), a function (an effect), a constant (written once)
const Demo = () => html`<div class="demo">
<div class="row">
<input aria-label="name" value="${name}" oninput=${(e: Event) => (name.value = (e.target as HTMLInputElement).value)} />
<label><input type="checkbox" checked="${loud}" onchange=${(e: Event) => (loud.value = (e.target as HTMLInputElement).checked)} /> loud</label>
</div>
<p class="${() => (loud.value ? 'loud' : '')}">hello, ${name}</p>
<p>rendered at ${new Date().toLocaleTimeString()} — a constant, written once</p>
</div>`;
const host = document.getElementById('demo');
if (host) mount(Demo, host);