Data, forms, and persistence
This page demos the Tier 1.5 surface: declarative fetch, forms that write state, persistent state, and the JavaScript escape hatch.
Fetched data drives reactive loops
:fetch team from "/__wd/data/team.json" declares state and fills it from the network. Until it lands, the page shows the fallback branch:
—
Team lead ★
Crew
Loading the team…
Loading the team…
That loop also proves :if over loop items: the lead badge is decided per row.
The four states of a fetch
Every :fetch tracks the whole lifecycle — loading, error, empty, and data — so a page can answer each one. Here the same primitive shows an error (a missing URL 404s) and an empty result (a file that is just []):
The request failed on purpose — broken_error holds:
Trying the missing endpoint…
Waiting…
Waiting…
Trying the missing endpoint…
Waiting…
Waiting…
The roster came back empty — @empty renders instead of the rows.
The roster came back empty — @empty renders instead of the rows.
The error branch uses an :else if chain; the empty branch uses the loop's @empty. No state machine to wire by hand.
Authenticated requests
A real API usually wants a token. :fetch … headers=<key> spreads a state object into the request headers, so pair it with :store to persist that token across reloads:
The API rejected the token:
Authenticated fetch returned row(s). Open the network tab — the Authorization header rides along on the request.
Loading the authenticated feed…
Loading the authenticated feed…
Authenticated fetch returned row(s). Open the network tab — the Authorization header rides along on the request.
Loading the authenticated feed…
Loading the authenticated feed…
This demo points from= at a static file (which ignores the header), so it stays self-contained — point it at your real API and the bearer token travels with every request. When that token expires, add refresh="/auth/refresh" and Darkmown renews it on a 401 and retries once, automatically.
Forms write state, no backend required
:form into profile captures the submit straight into state. With action="/url" instead, Darkmown emits a plain native form and stays out of the way entirely:
Hello — good luck with .
Submit the form and this sentence reacts. Nothing leaves the page.
Submit the form and this sentence reacts. Nothing leaves the page.
Server round-trips, adapter style
Darkmown does not own your backend — point a form at any endpoint. With JS, the submit becomes a fetch and the JSON reply lands in state; without JS, the same markup is a plain native POST:
The server answered with at .
The reply will appear here.
The reply will appear here.
The request failed:
In darkmown dev the echo endpoint is built into the dev server; on darkmown.com it is a real serverless function behind the same URL. On static-only hosts such as a plain Cloudflare Pages deploy, replace action="/__wd/echo" with your own API endpoint. Either way, Darkmown itself stays static — sessions ride on :fetch plus ordinary cookies against your real API.
Persistent state survives reload
:state items = [] persist keeps this section in localStorage. Add a few, reload the page, and the count holds:
The cart holds 0 item(s) worth $0.
:computed total = items.length * 4 derives state from state — the price updates with every cart change, persistence included.
The escape hatch
Colocated data.js uses the window.wd API (wd.get, wd.set) for anything directives can't say. Section state is addressed as cart:items:
Lazy loading
The list below uses when=visible — the request only fires once this section scrolls into view. Watch the network tab:
Scroll me into view and I will load.
Scroll me into view and I will load.
Richer form controls
:textarea, :select, :checkbox, and :radio join :input, all captured into the same :form into state with no backend. A :checkbox group captures every checked value as an array; a :radio group captures a single value:
Thanks — we logged your "" note. Services: . Urgency: .
Pick a topic and send — it is captured client-side, no backend.
Pick a topic and send — it is captured client-side, no backend.
Dynamic links from data, and link buttons
A build-time @loop can drive the href itself — [{ link.label }]({ link.url }) resolves the destination at compile time, so these stay plain static links:
A trailing {.class} styles a single link as a button — no wrapper container: