The single-file client (js/statum.js, served as
/_statum/resource/statum.js) is gone. The browser client now ships as four
ES5, dependency-free scripts with a strict load order:
<script src="/_statum/resource/statum-core.js"></script>
<script src="/_statum/resource/statum-document.js"></script>
<script src="/_statum/resource/statum-markdown.js"></script>
<script src="/_statum/resource/statum-forms.js"></script>
Load order is a contract. statum-core.js must be first: it owns the slot
protocol and installs the seams (statum.registerAttribute, the render
driver, the internal bridge) that every later module registers into. The
canonical order is core → document → markdown → forms.
You may cherry-pick trailing modules (core first, always): core + document
gives you the full state-binding walk (stm-text, stm-html, stm-if,
stm-loop, stm-action, …) with no markdown engine and no form extras.
Note that stm-markdown and stm-h-demotion live in the markdown module,
so a core+document page sees those attributes do nothing — remove them, or
load the markdown module.
Legacy pages that still say <script pstm-res-src="statum.js"></script>
keep working: statum-mkpstm expands that reference at build time into the
four tags above (one build warning per page names the expansion). A page
mixing the legacy reference with explicit module tags is deduplicated — each
module appears exactly once, in canonical order — and the warning says so.
The single-file artifact itself no longer exists; in new pages, just write
the four tags.
Depth on all of this: model.md (wire protocol, directives, every
attribute) and getting-started.md (tutorial + full JS API).
stm-prefill writes a state expression into form controls as properties,
not attributes, every render cycle:
<input type="text" name="name" stm-prefill="profile.name">
<select name="theme" stm-prefill="profile.theme">…</select>
<input type="checkbox" name="news" stm-prefill="profile.news">
The control matrix:
textarea — writes .value (null/undefined
become ''; undefined leaves the element untouched)..value; multiple: sets each option's
.selected from an array of string values..checked, coerced truthy/falsy.Radio — per-element: give every member of the group the same
expression and the one whose value matches is checked.
<input type="radio" name="cadence" value="daily" stm-prefill="profile.cadence">
<input type="radio" name="cadence" value="weekly" stm-prefill="profile.cadence">
File inputs are skipped: assigning .value from a script is forbidden
by the platform, so there is nothing sensible to write.
Dirty tracking is the point. The moment a user types in or changes a
control (capture-phase input/change listeners), it is dirty and
stm-prefill never overwrites it again — the edit outranks the state
snapshot. A form reset (a reset button, statum.forms.reset, or the
server's form-reset directive — all funnel through the same reset event)
un-dirties the form's controls so state can re-apply. Because dirtiness is
per-element (a WeakSet), a form inside a keyed stm-loop instance keeps
its dirty state across re-renders: the instance owns its nodes.
A form whose own stm-action succeeds can clear itself:
<form stm-action="profile.saveAction" stm-clear-on-success>…</form>
The clear waits for the action's promise and resets the form only when it
resolved (a failed action leaves the fields as they are). The reset also
un-dirties the controls, so the next render cycle re-applies stm-prefill
against the fresh state — and stm-clear-on-success is scoped to exactly
this form; no other form on the page is touched.
The statum.forms API:
statum.forms.reset(formOrElement); // reset the form owning the element
statum.forms.markDirty(el); // force an element dirty (prefill skips it)
statum.forms.isDirty(el); // query dirtiness
The CommonMark engine (unchanged, just moved) is now callable directly, on
the same code path the stm-markdown binding uses:
statum.markdown.render(src); // → safe HTML string
statum.markdown.renderTo(el, src); // replaces el's content
Escape-first posture unchanged: raw source HTML is neutralised and
javascript:/data: URLs never survive as hrefs.
Module authors extend the render walk without touching core:
statum.registerAttribute('stm-foo', function (el, scope) {
// called once per element per render cycle when el carries stm-foo
}, { after: 'stm-html' }); // or before: 'stm-class.'
A trailing dot makes the name a prefix matcher ('stm-class.' matches
stm-class.hidden); the handler then iterates the element's attributes
itself. after/before name another registered attribute and insert
relative to it — order matters (stm-prefill runs before stm-class.*;
markdown renders before stm-h-demotion demotes). A constraint naming an
unregistered attribute warns and keeps registration order; handlers get no
per-handler try/catch — a throwing handler aborts that render cycle, exactly
like the built-ins.
A response can ask the client to re-fetch the page's entrypoint for the current URL and apply its directives in place — no page reload:
{ "type": "reenter" }
Use it after an action that invalidates page-level state. reenter runs
after form-reset/form-submit and before navigate/post (navigation
still wins if both appear), and the refresh is awaited — the originating
action's promise (and so stm-clear-on-success and statum.act) resolves
only after the re-entered state has been applied. A failed refresh is logged
and swallowed.