---
html: true
title: Data, forms, and persistence
transitions: true
---

@include /nav.wd

<main>

# 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:

:fetch team from "/__wd/data/team.json"

:if team
@loop team into member
::: card .member-card
**{ member.name }**: { member.role }
:if member.lead
Team lead ★
:else
Crew
:endif
:::
@endloop
:else
Loading the team…
:endif

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 `[]`):

*(Heads up: the browser console will log a `404` for `intentionally-missing.json` on this page. That failure is deliberate, so the error branch below has something real to show.)*

:fetch broken from "/__wd/data/intentionally-missing.json"

:if broken_error
The request failed on purpose. `broken_error` holds: { broken_error }
:else if broken_loading
Trying the missing endpoint…
:else
Waiting…
:endif

:fetch roster from "/__wd/data/empty.json"

@loop roster into person
- { person.name }
@empty
The roster came back empty, so `@empty` renders instead of the rows.
@endloop

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:

:store session = { "Authorization": "Bearer demo-token" }
:fetch feed from "/__wd/data/team.json" headers=session

:if feed_error
The API rejected the token: { feed_error }
:else if feed
Authenticated fetch returned { feed.length } row(s). Open the network tab: the `Authorization` header rides along on the request.
:else
Loading the authenticated feed…
:endif

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:

:form into profile
:input name placeholder="Your name" required
:input quest placeholder="Your quest"
:submit "Save profile"
:endform

:if profile
Hello **{ profile.name }**, good luck with { profile.quest }.
:else
Submit the form and this sentence reacts. Nothing leaves the page.
:endif

## 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:

:form action="/api/echo/" into reply
:input message placeholder="Say something to the server"
:submit "Send to server"
:endform

:if reply
The server answered with { reply.received.message } at { reply.at }.
:else
The reply will appear here.
:endif

:if reply_error
The request failed: { reply_error }
:endif

In `darkmown dev` and `darkmown serve` the echo endpoint runs locally; 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="/api/echo/"` with your own API endpoint. Either way, Darkmown itself stays static, and 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:

::: section #cart
:state items = [] persist
:computed total = items.length * 4

The cart holds { items.length } item(s) worth ${ total }.

:button "Add sticker" -> items += {"id": "sticker", "name": "Sticker"}
:button "Add poster" -> items += {"id": "poster", "name": "Poster"}
:button "Empty cart" -> items = []
:::

`:computed total = items.length * 4` derives state from state, so 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`:

<button class="pulse-button" data-wd-double>Double the cart via window.wd</button>

## Lazy loading

The list below uses `when=visible`, so the request only fires once this section scrolls into view. Watch the network tab:

:fetch quotes from "/__wd/data/quotes.json" when=visible

:if quotes
@loop quotes into quote
- { quote }
@endloop
:else
Scroll me into view and I will load.
:endif

## 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:

:form into inquiry
:input fullname placeholder="Your full name"
:select topic
- General question
- New case
- Billing
:checkbox services
- Cleaning
- Whitening
:radio urgency
- Routine
- Urgent
:textarea message placeholder="How can we help?"
:submit "Send inquiry"
:endform

:if inquiry
Thanks **{ inquiry.fullname }**, we logged your "{ inquiry.topic }" note. Services: { inquiry.services }. Urgency: { inquiry.urgency }.
:else
Pick a topic and send. It is captured client-side, no backend.
:endif

## 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:

@loop /links.json into link
- [{ link.label }]({ link.url })
@endloop

A trailing `{.class}` styles a single link as a button, with no wrapper container:

[Read the docs](/docs/){.pulse-button}

</main>
