DOM
html``#
tagged template literals, CSP-safe, parsed once per template by a real HTML tokenizer. Prefixes everyone knows:
| Syntax | Meaning |
|---|---|
@click=${fn} |
addEventListener (also onclick=${fn}) |
.value=${sig} |
element property — <input .value>, <select .value>, <my-chart .points=${obj}> |
?disabled=${sig} |
boolean attribute (toggleAttribute) |
bind:value=${sig} |
two-way binding (bind:checked for checkboxes) |
:title=${sig} |
reactive attribute |
title="a ${x} ${y}" |
several values in one attribute |
<input ${ref}> |
ref() or (el) => … inside a tag |
<canvas ${attach(el => …)}> |
init + cleanup + reactivity for third-party widgets, runs after mount |
@submit.prevent, @keydown.enter, @click.outside, @keydown.esc.window, @input.debounce.300, @scroll.passive |
event modifiers (also prevent(fn) / stop(fn) / self(fn) helpers) |
class=${{ active, done: () => … }}, class=${['btn', size]} |
clsx-style classes with a diff: static and third-party classes are never touched, works on SVG |
style=${{ '--w': () => w.value + 'px', color }} |
per-property styles; custom properties go through setProperty (cssVars(el, { x }) for signal → CSS variable) |
${when(users, { loading, error, data })} |
three states of a resource in one place |
${() => cond ? html… : null} |
reactive children: a function may return text, a node, a template or an array — each run gets its own scope |
Bare value/checked/selected on form fields are treated as properties, so <input value=${sig}> keeps working after the user has typed.
show()#
conditional rendering with scope-per-branch (no leaks); { keep: true } hides instead of destroying (like v-show)
Components return their template#
mount(el, ({ html, slot }) => html\…`)replaces the element's content;slot()/slot('[slot=footer]')` hand server-rendered children to the template
Server HTML#
swap(el, response, { mode: 'morph' }) inserts server-rendered fragments the right way: islands in the replaced subtree are disposed, new ones hydrated, data-cloak removed, focus and caret restored; morph patches in place (id-aware, live islands keep their DOM and remount only when their data-* props change); a full page in the response is reduced to the target's selector
boost()#
MPA navigation without reload: fetch the next page, morph <main>, View Transitions, prefetch on hover; islands outside the root survive; data-no-boost opts out; a router() keeps its own routes
tpl('#card')#
a server <template> with data-slot becomes a render function for list()/show(): one markup for the first render and the client (no HTML parsing, CSP-safe)
adopt(el)html``#
bind a template to already server-rendered DOM with zero mutations (text, attributes, events, refs); dev warning when the server partial drifts from the template
Island props#
register(name, setup, { types: { count: Number, on: Boolean, tags: JSON } }); data-* stay strings unless typed (IDs never lose precision), <script type="application/json"> inside an island (Django json_script) or data-aegis-props="#id" lands in data.props; jsonScript('#config')
Lazy islands#
register('chart', { load: () => import('/js/islands/chart.js') }) or <div data-aegis="chart" data-aegis-src="/js/islands/chart.js" data-aegis-load="visible">: island code is fetched only when needed (400px before a visible island enters the viewport); data-aegis-prefetch="/api/…" warms its data too
Routing 2.0#
nested routes with layout and a live outlet, async handler/loader awaited inside intercept() (native loading indicator, scroll after data, races cancelled via signal), guard/redirect as route data, r.search('page', { parse: Number }) two-way URL signals that do not recreate the route scope, lazy routes via load: () => import(…) with hover/visible preload, aria-current + activeClass on server-rendered links, transition: true View Transitions with back/forward direction; hash links, forms, downloads, data-aegis-reload and unknown paths go to the server
Forms 2.0#
wireForm validates through the Constraint Validation API (noValidate, setCustomValidity, browser-localised messages, :user-invalid), accepts a Standard Schema (zod/valibot/arktype) next to per-field rules, async rules with validating[key], debounce and stale-response cancellation, nested names (items[0][qty], address.city) in values/setErrors, setInitial(resource.data) / commit() / dirtyFields / changes / guardUnload(), server submit with FormData (422 → field errors, 303 → navigation); form(defaults, { rules, schema }) for virtual forms
hydrate(root, { watch: true })#
islands inserted by htmx/Turbo/jQuery come alive, removed ones are destroyed; register() auto-hydrates the document; aegis:hydrate / aegis:hydrated / aegis:destroy events; eager islands mount in ~8 ms slices between tasks
Event handlers are batched#
on(), delegate(), @click: five signal writes in a handler, one flush
Effects run under their owner#
whatever created the effect owns everything it creates on every re-run (getOwner() / runWithOwner() for code after await)
list()#
keyed reconciliation with prefix/suffix trim, DocumentFragment batch insert; a row is exactly the nodes you return (<tr> inside <tbody>, <option> inside <select>, <dt>+<dd>) — no wrapper elements; a row re-renders when the object behind its key is replaced, index is a lazy signal, duplicate/missing keys never drop rows
bind()#
two-way binding (checkbox, radio, select, number)
text(), attr(), cls(), style()#
single-property reactive bindings
clsMap(), styleMap()#
multi-property reactive bindings
portal()#
render to a different DOM location
ref()#
DOM element references