|
@@ -0,0 +1,308 @@
|
|
|
|
|
+# TEMPLATES.md — Statum template and markup reference
|
|
|
|
|
+
|
|
|
|
|
+This document is the reference for every tag, attribute and expression form
|
|
|
|
|
+that is special to Statum: the `pstm-*` preprocessor syntax handled at build
|
|
|
|
|
+time by `statum-mkpstm`, and the `stm-*` attributes handled at runtime by the
|
|
|
|
|
+four browser modules (`statum-core.js`, `statum-document.js`,
|
|
|
|
|
+`statum-markdown.js`, `statum-forms.js`).
|
|
|
|
|
+
|
|
|
|
|
+Audience: a developer writing Statum pages. For the wire protocol, directives
|
|
|
|
|
+and the Vala API see `model.md`; for a tutorial see `getting-started.md`.
|
|
|
|
|
+
|
|
|
|
|
+Two namespaces, two phases:
|
|
|
|
|
+
|
|
|
|
|
+- `pstm-*` — resolved at BUILD time by `statum-mkpstm`. Nothing with a
|
|
|
|
|
+ `pstm-` prefix is supposed to reach the browser; the delivered page is a
|
|
|
|
|
+ plain static document. If one leaks through anyway, the client logs a
|
|
|
|
|
+ console warning listing them.
|
|
|
|
|
+- `stm-*` — resolved at RUNTIME by the client modules. These attributes stay
|
|
|
|
|
+ in the delivered HTML and are re-evaluated on every render cycle.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+## Part I — The pstm preprocessor (build time)
|
|
|
|
|
+
|
|
|
|
|
+`statum-mkpstm` turns one page file plus any templates and fragments it names
|
|
|
|
|
+into a Vala `StatumPage` subclass: a composed, precompressed HTML document
|
|
|
|
|
+with a baked-in route. Composition is: load templates → merge heads upward →
|
|
|
|
|
+inline fragments (repeat until none remain) → expand the legacy client
|
|
|
|
|
+reference → rewrite `pstm-res-*` → strip every remaining `pstm-*` → serialise
|
|
|
|
|
+as HTML → precompress (identity/gzip/zstd/brotli).
|
|
|
|
|
+
|
|
|
|
|
+CLI: `statum-mkpstm -o OUT.vala -n ClassName --ns=Namespace [-r ROUTE]
|
|
|
|
|
+[-t DIR ...] PAGE.html`. The route comes from the page's `<pstm-uri>` unless
|
|
|
|
|
+`-r` overrides it.
|
|
|
|
|
+
|
|
|
|
|
+### `<pstm-uri>` — the page route (element, page head)
|
|
|
|
|
+
|
|
|
|
|
+Child element of the page's `<head>` whose text is the route this page is
|
|
|
|
|
+served at (e.g. `<pstm-uri>/dashboard</pstm-uri>`). The generated class's
|
|
|
|
|
+`route` comes from here; registering a page whose composition dropped or
|
|
|
|
|
+omits the `pstm-uri` throws `StatumError.CONFIGURATION` at startup. Routes are
|
|
|
|
|
+read BEFORE head merging because merging consumes `pstm-uri` upward.
|
|
|
|
|
+
|
|
|
|
|
+### `<pstm-name>` — the page name (element, page head)
|
|
|
|
|
+
|
|
|
|
|
+Names the page for build diagnostics. Structural: consumed during
|
|
|
|
|
+composition, never delivered.
|
|
|
|
|
+
|
|
|
|
|
+### `<pstm-title>` — title contribution (element, page head)
|
|
|
|
|
+
|
|
|
|
|
+The page's title fragment. Titles CONCATENATE page-first: a page's
|
|
|
|
|
+`<pstm-title>` text is prepended to its template's title. Merged like all
|
|
|
|
|
+head content, then consumed.
|
|
|
|
|
+
|
|
|
|
|
+### `<pstm-template>` — parent template (element, page head)
|
|
|
|
|
+
|
|
|
|
|
+Names the template file (without extension) this page composes into. The
|
|
|
|
|
+template is looked up in the page's directory or any `-t` template dir.
|
|
|
|
|
+A template is itself an HTML file whose `<body>` contains a
|
|
|
|
|
+`<pstm-content>` outlet. Chains (a template naming a `<pstm-template>` of its
|
|
|
|
|
+own) compose recursively. An unknown template name is a build ERROR; a cyclic
|
|
|
|
|
+chain is not diagnosed — keep chains acyclic.
|
|
|
|
|
+
|
|
|
|
|
+### `<pstm-content>` — content outlet (element, template body)
|
|
|
|
|
+
|
|
|
|
|
+The element inside a template's `<body>` that the child's `<body>` content is
|
|
|
|
|
+spliced into, replacing the outlet. A template without an outlet is a build
|
|
|
|
|
+error (there is nowhere to splice).
|
|
|
|
|
+
|
|
|
|
|
+### `<pstm-fragment name="…">` — fragment inclusion (element, any body)
|
|
|
|
|
+
|
|
|
|
|
+Includes a named fragment. Fragments live in their own files and are declared
|
|
|
|
|
+inside `<pstm-fragment name="…"> … </pstm-fragment>` wrapper elements; the
|
|
|
|
|
+include element itself carries `name="…"` naming the fragment to inline.
|
|
|
|
|
+Inclusion is recursive and runs to a fixed point: a fragment may include
|
|
|
|
|
+fragments. Nested fragment markup is inlined where the include element sits.
|
|
|
|
|
+An unknown fragment name is silently left as-is (and then stripped with all
|
|
|
|
|
+other `pstm-*` elements) — check your builds for missing content rather than
|
|
|
|
|
+errors.
|
|
|
|
|
+
|
|
|
|
|
+A fragment's `<head>` content is merged into the document head like a
|
|
|
|
|
+template's. A fragment `<body>` may carry metadata elements such as
|
|
|
|
|
+`<pstm-input>` (consumed, not delivered).
|
|
|
|
|
+
|
|
|
|
|
+### `pstm-materialise-as` — wrapper element (attribute)
|
|
|
|
|
+
|
|
|
|
|
+On a page's `<body>` or a fragment's `<body>` or a `<pstm-fragment>` include:
|
|
|
|
|
+wraps the spliced/inlined content in a single element of the given tag
|
|
|
|
|
+(e.g. `pstm-materialise-as="section"`). Fragments that declare inputs are
|
|
|
|
|
+ALWAYS wrapped (a `div` by default) so the inputs have a stable container.
|
|
|
|
|
+When the fragment body supplied the tag (rather than the include site), the
|
|
|
|
|
+fragment body's non-`pstm-*` attributes are copied onto the wrapper. An
|
|
|
|
|
+explicit attribute at the include site wins over the fragment's own.
|
|
|
|
|
+
|
|
|
|
|
+### `pstm-res-src` / `pstm-res-href` (and any `pstm-res-{attr}`) — resource URL (attribute)
|
|
|
|
|
+
|
|
|
|
|
+Rewrites to `{attr}="/_statum/resource/{value}"`. `<script
|
|
|
|
|
+pstm-res-src="statum-core.js">` becomes `<script
|
|
|
|
|
+src="/_statum/resource/statum-core.js">`. Works for any attribute name
|
|
|
|
|
+(`src`, `href`, ...). The named resource must be registered server-side
|
|
|
|
|
+(`StatumConfigurator.add_resource` or the four default client modules).
|
|
|
|
|
+
|
|
|
|
|
+### The legacy `statum.js` expansion (script tags)
|
|
|
|
|
+
|
|
|
|
|
+A `<script pstm-res-src="statum.js">` — the pre-split single-file client —
|
|
|
|
|
+expands at build time into the four module tags in canonical load order:
|
|
|
|
|
+
|
|
|
|
|
+```html
|
|
|
|
|
+<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>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+One build warning per page names the page and the expansion. Pages that
|
|
|
|
|
+reference module files directly are untouched and unwarned. 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. Plain
|
|
|
|
|
+`src="…"` scripts are never touched. In new pages, write the four tags
|
|
|
|
|
+explicitly (see Part II for the load contract).
|
|
|
|
|
+
|
|
|
|
|
+### What never reaches the browser
|
|
|
|
|
+
|
|
|
|
|
+Every `pstm-*` attribute and element is stripped after composition. The
|
|
|
|
|
+delivered page is plain HTML + `stm-*` runtime attributes. See `model.md` for
|
|
|
|
|
+the wire protocol and directives.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+## Part II — The runtime modules
|
|
|
|
|
+
|
|
|
|
|
+Load contract: **core first, always**. `statum-core.js` owns the slot
|
|
|
|
|
+protocol, the directive pipeline, the realtime channel and the seams
|
|
|
|
|
+(`statum.registerAttribute`, the render driver, the internal
|
|
|
|
|
+`statum._bridge`) that every later module registers into. The canonical
|
|
|
|
|
+order is core → document → markdown → forms. Trailing modules may be
|
|
|
|
|
+cherry-picked after core (e.g. core + document for pages without markdown or
|
|
|
|
|
+form extras — note `stm-markdown` AND `stm-h-demotion` live in the markdown
|
|
|
|
|
+module). A module loaded without core logs `console.error('[statum] …
|
|
|
|
|
+requires statum-core.js')` and does nothing else. All modules are ES5,
|
|
|
|
|
+dependency-free, guarded IIFEs.
|
|
|
|
|
+
|
|
|
|
|
+### Expression syntax (all modules)
|
|
|
|
|
+
|
|
|
|
|
+`stm-*` attribute values are JavaScript expressions evaluated against a scope
|
|
|
|
|
+built from state: `{ typeName: public }` for every held slot type, plus loop
|
|
|
|
|
+variables and `stm-def-*` locals in nested scopes. `statum.read('counter')`
|
|
|
|
|
+is the programmatic equivalent. Expression compilation is cached. An
|
|
|
|
|
+expression evaluating to `undefined` means "not resolved yet" and LEAVES THE
|
|
|
|
|
+ELEMENT UNTOUCHED (the server-rendered original DOM is preserved — this is
|
|
|
|
|
+the progressive-enhancement policy); `null` is stringified to `''`.
|
|
|
|
|
+
|
|
|
|
|
+The scope is a `Proxy` over the state object with `with`-semantics — slot
|
|
|
|
|
+type names are resolved first, then globals. Prefer distinct type names.
|
|
|
|
|
+
|
|
|
|
|
+### Render cycles
|
|
|
|
|
+
|
|
|
|
|
+Every `set`/`clear` (directive, cross-tab broadcast, SSE push, or
|
|
|
|
|
+`statum.clear()`) triggers `renderAll()`: `beforeEvaluate` hooks → the
|
|
|
|
|
+document walk → `afterEvaluate` hooks → guard evaluation. The walk visits
|
|
|
|
|
+every element; each registered attribute handler runs for elements carrying a
|
|
|
|
|
+matching attribute. The walk is stateless — no binding remembers its previous
|
|
|
|
|
+value; anything your handler writes is rewritten on the next cycle unless the
|
|
|
|
|
+handler itself guards (see `stm-prefill`'s dirty tracking for the pattern).
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+### Section: statum-core.js
|
|
|
|
|
+
|
|
|
|
|
+Slot protocol, HTTP, directives, realtime, boot, public JS API. Attributes it
|
|
|
|
|
+owns:
|
|
|
|
|
+
|
|
|
|
|
+- **`stm-entrypoint`**, **`stm-slots`**, **`stm-channel`**, **`stm-worker`**
|
|
|
|
|
+ (on `<body>`): override the four endpoint URLs (defaults
|
|
|
|
|
+ `/_statum/entrypoint`, `/_statum/slots`, `/_statum/channel`,
|
|
|
|
|
+ `/_statum/resource/statum-worker.js`). Read once at init.
|
|
|
|
|
+- **`stm-preloader`** / **`stm-content`**: boot visibility. While the
|
|
|
|
|
+ entrypoint loads, `stm-preloader` elements are shown and `stm-content`
|
|
|
|
|
+ elements hidden; once ready, the reverse. Set `hidden` on `stm-content`
|
|
|
|
|
+ yourself in the HTML to avoid a flash of unbound template.
|
|
|
|
|
+- **`stm-guard`** (a `<link rel="stm-guard" require="…" href="…">`, plus the
|
|
|
|
|
+ `pre-entrypoint` attribute on the link): a client-side UX gate — when
|
|
|
|
|
+ `require` evaluates false, the page redirects to `href`. Re-checked every
|
|
|
|
|
+ render cycle; guards without `pre-entrypoint` only evaluate after the
|
|
|
|
|
+ entrypoint has loaded. This is UX sugar, NOT security.
|
|
|
|
|
+- **`stm-on-error`** (on action elements): space-separated class names
|
|
|
|
|
+ applied while the action's request is failing / after an `error` directive.
|
|
|
|
|
+- **`stm-class.{name}`**: class-list predicates shared by core's busy-state
|
|
|
|
|
+ logic and the document walk (see below for semantics).
|
|
|
|
|
+- **`stm-disabled`**: predicate disabling the element and its descendant form
|
|
|
|
|
+ controls.
|
|
|
|
|
+
|
|
|
|
|
+Directives (server → client, `application/vnd.statum+json`): `set`, `clear`,
|
|
|
|
|
+`invalidate`, `subscribe`, `unsubscribe`, `notify`, `transmit`, `error`,
|
|
|
|
|
+`navigate`, `reenter`, `post`, `form-submit`, `form-reset`. `reenter`
|
|
|
|
|
+re-fetches the entrypoint in place (no page reload), after form directives
|
|
|
|
|
+and before navigate/post. See `model.md` for payloads.
|
|
|
|
|
+
|
|
|
|
|
+Public JS API (on `window.statum`): the essentials are `version`, `config`,
|
|
|
|
|
+`init`, `read`, `getSnapshot`, `getFrame`, `state`, `clear`, `act`,
|
|
|
|
|
+`addStateListener` / `removeStateListener`, `beforeEvaluate` /
|
|
|
|
|
+`afterEvaluate`, `Error`, the `onConfirmHandler` / `onNotifyHandler` /
|
|
|
|
|
+`onErrorHandler` getter/setter pairs (the override mechanism for
|
|
|
|
|
+`stm-confirm` / `notify` / failures — frontend-demo installs toast handlers
|
|
|
|
|
+through it), and `registerAttribute` (below).
|
|
|
|
|
+
|
|
|
|
|
+**`statum.registerAttribute(name, fn, {after, before})`** — the extension
|
|
|
|
|
+seam. `name` is an exact attribute name or, ending with `.`, a prefix
|
|
|
|
|
+matcher. `fn(el, scope)` runs once per element per render cycle for elements
|
|
|
|
|
+carrying a match; iterate `el.attributes` yourself when several match a
|
|
|
|
|
+prefix. `after`/`before` name another registered attribute and position the
|
|
|
|
|
+handler relative to it; an anchor that is not (yet) registered warns and
|
|
|
|
|
+keeps registration order — order is resolved at registration time, so a
|
|
|
|
|
+module that wants to be after `stm-html` should load after statum-document.
|
|
|
|
|
+Duplicate names warn and are ignored. Handler exceptions abort that render
|
|
|
|
|
+cycle (caught by renderAll's single try/catch) — same contract as the
|
|
|
|
|
+built-ins.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+### Section: statum-document.js
|
|
|
|
|
+
|
|
|
|
|
+The render walk and the structural/value bindings. Installs the render
|
|
|
|
|
+driver. Attributes:
|
|
|
|
|
+
|
|
|
|
|
+- **`stm-if` / `stm-else-if` / `stm-else`**: conditional chains among
|
|
|
|
|
+ siblings. Branches are toggled with the `hidden` attribute (they stay in
|
|
|
|
|
+ the DOM and are re-evaluated every cycle). `stm-else-if`/`stm-else` are
|
|
|
|
|
+ owned by their chain head's `stm-if`.
|
|
|
|
|
+- **`stm-for-{name}-in`** (with optional **`stm-key`**): loop over an array
|
|
|
|
|
+ expression. The element is a HOLDER: its original server-rendered content
|
|
|
|
|
+ is hidden once data arrives and clones of the holder are inserted after it,
|
|
|
|
|
+ one per item, with `{name}` bound to the item in child scopes. Unresolved
|
|
|
|
|
+ (`undefined`) source: instances are cleared and the holder's original
|
|
|
|
|
+ content restored (progressive enhancement). Keyed loops (`stm-key`, an
|
|
|
|
|
+ expression over the item, usually a field) REUSE DOM nodes for unchanged
|
|
|
|
|
+ keys, preserving focus/selection/input state; unkeyed loops rebuild
|
|
|
|
|
+ wholesale. `data-stm-active="1"` + `hidden` mark an active holder.
|
|
|
|
|
+- **`stm-def-{name}-as`**: local definition. The expression is evaluated
|
|
|
|
|
+ against the incoming scope and the result is visible to this element and
|
|
|
|
|
+ its descendants (a child scope). Multiple defs per element; defs cannot
|
|
|
|
|
+ reference each other within the same element. Used heavily by generated
|
|
|
|
|
+ fragment markup.
|
|
|
|
|
+- **`stm-text`** / **`stm-html`**: set text content / inner HTML from an
|
|
|
|
|
+ expression. `undefined` leaves content untouched; otherwise stringified
|
|
|
|
|
+ (`null` → `''`). Note (known issue C1): on editable controls `stm-text`
|
|
|
|
|
+ clobbers user input every cycle — use `stm-prefill` for form fields.
|
|
|
|
|
+- **`stm-class.{name}`**: predicate per class. Truthy → `classList.add`,
|
|
|
|
|
+ falsy → remove.
|
|
|
|
|
+- **`stm-attribute.{name}`**: boolean → set/remove (preferring the IDL
|
|
|
|
|
+ property when one exists); any other defined value → `setAttribute`;
|
|
|
|
|
+ `undefined` → remove. Note (known issue C2): for `value` this writes the
|
|
|
|
|
+ ATTRIBUTE (the default), not the live property — server-wins semantics;
|
|
|
|
|
+ live form values want `stm-prefill`.
|
|
|
|
|
+- **`stm-disabled`**: truthy → disable the element and descendant controls.
|
|
|
|
|
+- **`stm-action`**: binds the element's primary event (`click`, or `submit`
|
|
|
|
|
+ for a `<form>`) to an action object resolved from state. The action object
|
|
|
|
|
+ (shape `{ uri, method, private }`) is authored server-side and embedded in
|
|
|
|
|
+ slot data. Companions:
|
|
|
|
|
+ - **`stm-data-{key}`**: string data collected into the action's payload.
|
|
|
|
|
+ - **`stm-confirm`**: gates the action behind the confirm handler
|
|
|
|
|
+ (`statum.onConfirmHandler`; default `window.confirm`).
|
|
|
|
|
+ - **`stm-busy-class`**: classes applied to the action element while busy.
|
|
|
|
|
+ - **`stm-on-error`**: see core section.
|
|
|
|
|
+ The element's `__stmAction` promise (and `__stmActionFailed` flag) are
|
|
|
|
|
+ stashed per trigger for `stm-clear-on-success`.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+### Section: statum-markdown.js
|
|
|
|
|
+
|
|
|
|
|
+The CommonMark 0.31.2 engine (escape-first: raw source HTML is always
|
|
|
|
|
+neutralised; `javascript:`/`data:`/`vbscript:` URLs are filtered) plus its
|
|
|
|
|
+bindings:
|
|
|
|
|
+
|
|
|
|
|
+- **`stm-markdown`**: renders the expression's markdown to safe HTML into the
|
|
|
|
|
+ element (`innerHTML`). Runs after `stm-html`.
|
|
|
|
|
+- **`stm-h-demotion`**: demotes child header tags (`h1`→`h2` …) by N levels,
|
|
|
|
|
+ capping at `h6`. Runs after `stm-markdown` so rendered headers exist. Both
|
|
|
|
|
+ live in this module — a core+document page sees them do nothing.
|
|
|
|
|
+- Programmatic: **`statum.markdown.render(src)`** → HTML string;
|
|
|
|
|
+ **`statum.markdown.renderTo(el, src)`** — same code path as the binding.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+### Section: statum-forms.js
|
|
|
|
|
+
|
|
|
|
|
+Form-state bindings:
|
|
|
|
|
+
|
|
|
|
|
+- **`stm-prefill`**: writes an expression into form controls as PROPERTIES
|
|
|
|
|
+ every cycle: text-like inputs + textarea → `.value` (null → `''`);
|
|
|
|
|
+ checkbox → `.checked` (truthiness); radio → `.checked = (el.value ===
|
|
|
|
|
+ String(value))` — give every member of a radio group the same
|
|
|
|
|
+ `stm-prefill` expression; select (single) → `.value`; select (multiple) →
|
|
|
|
|
+ per-option `.selected` when the value is an array. `file` inputs are
|
|
|
|
|
+ skipped (script assignment is forbidden by browsers). DIRTY TRACKING:
|
|
|
|
|
+ capture-phase `input`/`change` mark the element dirty; a dirty element is
|
|
|
|
|
+ NEVER overwritten by prefill; the form's `reset` event un-dirties its
|
|
|
|
|
+ controls (also the server `form-reset` directive's path). Dirty state is
|
|
|
|
|
+ per-element, so keyed-loop forms keep their own dirty state per instance.
|
|
|
|
|
+- **`stm-clear-on-success`** (on a `<form>` with its own `stm-action`): when
|
|
|
|
|
+ the form's action promise resolves successfully, reset exactly THAT form
|
|
|
|
|
+ (values back to defaults, un-dirtied). On failure the fields stay. A form
|
|
|
|
|
+ carrying it without `stm-action` warns once.
|
|
|
|
|
+- Programmatic: **`statum.forms.reset(formOrEl)`**, **`statum.forms.markDirty(el)`**,
|
|
|
|
|
+ **`statum.forms.isDirty(el)`**.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+## Part III — Quick reference: load order and cherry-picking
|
|
|
|
|
+
|
|
|
|
|
+Canonical: core → document → markdown → forms. Cherry-pick trailing modules
|
|
|
|
|
+only. Without markdown: no `stm-markdown`, no `stm-h-demotion`. Without
|
|
|
|
|
+forms: no `stm-prefill`, no `stm-clear-on-success`, no `statum.forms`. All
|
|
|
|
|
+modules assume core; loading any module first logs an error and does nothing.
|