WHATS_NEW.md 9.8 KB

What's new — headless core, four modules, client-side variables

The browser client now ships as four ES5, dependency-free scripts with a strict load order — and core is headless:

<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-binding.js"></script>

Load order is a contract. statum-core.js must be first: it owns the slot protocol (state store, persistence, directives, SSE/SharedWorker realtime, entrypoint/action requests) and creates window.statum. The canonical order is core → document → markdown → binding (markdown and binding are order-independent of each other). The former statum-forms.js module was folded into statum-document.js (and its resource now 404s).

Core is headless — usable without any DOM

statum-core.js no longer touches the DOM at all. Everything visual — the render walk, the attribute registry, boot visibility, stm-guard links, error/success classes, form bindings — lives in statum-document.js, which installs itself into core through internal hooks. Core alone boots, fetches the entrypoint and applies directives to state, so it works as a state layer for non-document templating frameworks (or plain JS):

<script src="/statum-core.js"></script>
<script>
  statum.config.entrypointUrl = '/api/entrypoint'; // before init runs
  statum.addStateListener('profile', function (ev) { renderYourWay(ev); });
</script>

Endpoint config still arrives from the page too: statum-document.js reads the stm-entrypoint / stm-slots / stm-channel / stm-worker <body> attributes into statum.config during its load, before core's init runs. Cherry-picking still works (core first, always): a headless page loads only core; a page without markdown loads core+document.

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; explicit module tags are deduplicated). The single-file artifact itself no longer exists.

Depth on all of this: model.md (wire protocol, directives, every attribute) and getting-started.md (tutorial + full JS API).

stm-prefill — state into controls, without clobbering edits

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:

  • Text-like inputs and textarea — writes .value (null/undefined become ''; undefined leaves the element untouched).
  • Select — single: writes .value; multiple: sets each option's .selected from an array of string values.
  • Checkbox — writes .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.document.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 loop instance keeps its dirty state across re-renders: the instance owns its nodes.

Mirrored form outcomes: stm-clear-on-success, stm-clear-on-error, stm-on-success

A form whose own stm-action settles can clear itself on either outcome:

<form stm-action="profile.saveAction" stm-clear-on-success>…</form>
<form stm-action="profile.retryAction"  stm-clear-on-error>…</form>

Both wait for the action's promise and read its outcome flag: success resets a stm-clear-on-success form, a real failure (HTTP error or error directive) resets a stm-clear-on-error form, and neither touches any other form on the page. A declined stm-confirm never clears — decline is cancellation, not error. The reset also un-dirties the controls, so the next render cycle re-applies stm-prefill against the fresh state.

Action elements gained the success mirror of stm-on-error:

<button stm-action="profile.saveAction" stm-on-success="ok" stm-on-error="error">
  Save
</button>

The two are mirrored: a success applies the success classes and clears the error classes; a failure does the reverse; a new attempt clears both at its start.

The programmatic form API lives on statum.document now (statum.forms is gone — the module merge made it redundant):

statum.document.reset(formOrElement); // reset the form owning the element
statum.document.markDirty(el);        // force an element dirty (prefill skips it)
statum.document.isDirty(el);          // query dirtiness

statum.markdown — the renderer as an API

The CommonMark engine (unchanged, just moved) is 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.

statum.registerAttribute — write your own bindings

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.

The registry is provided by statum-document.js (it is the walk's machinery). Loading a registering module against core alone is safe: core queues the registration until document loads.

Client-side variables — the binding module

statum-binding.js (fourth script tag, order-independent of markdown) adds variables usable in any other stm-* expression. All are lazy-cached: a binding evaluates at most once per render pass, only when read; cycles log an error and read as undefined instead of crashing the page.

<div stm-global-vat="0.2"></div>          <!-- page-wide, any position -->
<div stm-local-total="price * (1 + vat)">  <!-- this subtree only -->
  <span stm-text="total"></span>
</div>

<div stm-declare-username>
  <input stm-local-username-from-value>   <!-- live property; redraws per keystroke -->
  <p stm-if="username.length < 3">Username is too short</p>
</div>

-from-{source} forms source the variable from a live property or attribute. When {source} names a registered observable property (value and checked ship by default) the binding reads the live property and listens to its change events — typing updates the variable and redraws per keystroke — and further event-driven properties register generically:

statum.binding.observe('selectedIndex', ['change']);

Anything else is read as an ATTRIBUTE — prefer the explicit -from-attribute.{name} spelling, aligned with stm-attribute.{name} — and observed via MutationObserver (redraw only when the value actually changes; the walk's own attribute writes never loop). stm-declare-{name} reserves a name for a subtree so a descendant can bind it and siblings of the binder can read it.

stm-update-attribute.{name}="expr" is the write-side counterpart: it sets the attribute only when the expression's EVALUATED value differs from what the binding last wrote, so local changes (a user-toggled <details open>) survive between renders:

<details stm-update-attribute.open="section.open">…</details>

Migration: stm-def-{name}-as still works as an exact obsolescent alias of stm-local-{name} (one console.warn per element), so older templates keep running; it requires the binding module like the modern form. statum-mkpstm emits stm-local-{name} for pstm-input fragment parameters.

The reenter directive

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.

Retired

The former stm-def-{name}-as attribute is an obsolescent alias of stm-local-{name} (still works, warns once) — see the binding section. The single-file client (statum.js) remains gone: legacy references still expand at build time into the four module tags above.