Date range forms Endpoint
Two native date inputs driving one htmx exchange — the from/to filter bar for time-scoped regions.
Layer: L1 surface · Recipe: unset — see docs/agent/pick-a-surface.md. Curriculum: AGENTS.md; pick matrix: docs/agent/pick-a-surface.md; blast radius: CONSUMER_MAP.md.
Copy this
<div class="date-range-picker date-range-bar" data-date-range>
<label class="date-range-label" for="hm-dr-from">From</label>
<input type="date" id="hm-dr-from" name="date_from" value="2026-06-01" class="date-range-input" hx-get="/mock/search" hx-target="#hm-dr-out" hx-swap="innerHTML" hx-include="closest .date-range-bar">
<label class="date-range-label" for="hm-dr-to">To</label>
<input type="date" id="hm-dr-to" name="date_to" value="2026-06-30" class="date-range-input" hx-get="/mock/search" hx-target="#hm-dr-out" hx-swap="innerHTML" hx-include="closest .date-range-bar">
<div id="hm-dr-out" hidden></div>
</div>Server exchange
When the client affordance finishes (click, confirm, keystroke…), htmx issues this request. Your API must return the response fragment in the table — usually HTML, not JSON (unless the partial says otherwise). Dazzle often renders these routes from the app model; a standalone HTMX4 app implements them explicitly. The Envelope column is the exchange envelope (part of the Swap contract) — what the response may re-emit relative to the persistent slot.
Do not reimplement the gallery. Flash toasts (e.g. “Deleted (demo).”), /mock/* paths, and other static-site scaffolding are demo-only (MOCK_HTMX in site/build_site.py). They are not Hyperpart surface and not a product API. If an agent is stuck “making the toast work,” stop — implement the exchange row below instead.
| Request | Trigger | Response fragment | Swap | Envelope | States |
|---|---|---|---|---|---|
GET /app/{region}?date_from=&date_to= | either date input's change — hx-include sends both bounds | the re-rendered region body for the new range | innerHTML | body_only | — |
Swap contract
Agent-visible HTMX topology (ADR-0054 / decision 0012). exchange envelope = what the response may re-emit relative to the persistent slot (body_only | outer | none | host_owned | document). dual-lock validates part markup only — not this envelope. Stem: stems/morph-safe-hypermedia.md; decision: docs/decisions/0012-swap-identity-contract.md.
Gallery mocks may approximate morph with innerHTML — production follows the Swap + Envelope columns in Server exchange.
Exchanges (swap · envelope)
GET /app/{region}?date_from=&date_to=→ innerHTML · envelope=body_only
Envelope rules
body_only— innerHTML / innerMorph into a slot; response is interior only (no re-wrap of slot id / nesteddata-dz-region).outer— outerHTML / outerMorph; response may carry identity.none— no HTML swap (JSON/204/bytes; client or OOB companion).host_owned— swap target/mode chosen by the host button’shx-target/hx-swap.document— full navigation / document load (not a fragment).- Slot owns stable
id/ domain keys; state in DOM, not Alpine.
Envelope response examples
What the server returns for each exchange on Date range. Match the exchange envelope; dual-lock still applies to interior markup.
GET /app/{region}?date_from=&date_to= · envelope=body_only
Correct response for body_only into #{region}-body (innerHTML / innerMorph). Wrong: re-wrapping the slot.
Do — correct response body
<!-- envelope=body_only → re-rendered region body for the range -->
<div class="dz-stack" data-dz-gap="sm">
<!-- metrics / rows for date_from..date_to -->
</div>
Don’t — violates body_only
<!-- WRONG: date-range chrome re-emitted into the region body -->
<div id="{region}-body" data-dz-region>
<input type="date" name="date_from" />
<input type="date" name="date_to" />
</div>
How to use it
No extended guidance authored yet — start from Copy this and the dependency chips (Primitive = markup only; controller = load listed JS; Endpoint = implement Server exchange).
Seams
- copy the partial under Copy this; keep root class and data-* modifiers so the CSS/JS bundle matches
- implement Server exchange endpoints; return HTML fragments, not JSON
- satisfy the DOM contract tables (CI stop-ship)
DOM contract
What the emitted HTML must satisfy — the table is the required surface; Python under contracts/ is the package-internal dual-lock CI runs (tests/test_contracts.py), not an app route. Standalone HTMX4: implement the API so responses match this markup. Dazzle: the agent emits SSR that already satisfies it. Do not invent attrs outside these tables. For request/response wiring see Server exchange.
contracts/date_range.py
Required in the DOM: root [data-date-range] (part date-range). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).
| Node | Attr | Constraint |
|---|---|---|
[data-date-range] | data-date-range | present (any value) |
Ingestion model DateRange
Server-side shape before render — one normalisation boundary for producers.
| Field | Type | Required |
|---|---|---|
region_name | string | optional |
endpoint | string | optional |
date_from | string | optional |
date_to | string | optional |
target | string | optional |
Exemplar render()
Executable in CI: the Python below is render(); the boxed preview is render(EXEMPLARS[0]) — the first fixture the dual-lock tests emit, not a separate widget and not gallery mock data.
def render(d: DateRange) -> str:
"""Model → date-range picker bar."""
rname = html.escape(d.region_name, quote=True)
endpoint = html.escape(d.endpoint, quote=True)
target = html.escape(d.target or f"#region-{d.region_name}", quote=True)
date_from = html.escape(d.date_from, quote=True)
date_to = html.escape(d.date_to, quote=True)
return (
f'<div class="dz-date-range-picker date-range-bar" data-dz-date-range>'
f'<label class="dz-date-range-label" for="date-from-{rname}">From</label>'
f'<input type="date" id="date-from-{rname}" name="date_from" '
f'value="{date_from}" class="dz-date-range-input" '
f'hx-get="{endpoint}" hx-target="{target}" hx-swap="innerHTML" '
f'hx-include="closest .date-range-bar">'
f'<label class="dz-date-range-label" for="date-to-{rname}">To</label>'
f'<input type="date" id="date-to-{rname}" name="date_to" '
f'value="{date_to}" class="dz-date-range-input" '
f'hx-get="{endpoint}" hx-target="{target}" hx-swap="innerHTML" '
f'hx-include="closest .date-range-bar">'
f"</div>"
)
Live output of render(EXEMPLARS[0]) — fixture markup the dual-lock validates (sample field values only).
Notes
data-date-range (contracts/date_range.py). Native type="date" inputs — no picker JS. Each input fires the region's hx-get on change and hx-include="closest .date-range-bar" sends BOTH bounds every time, so the server always sees the full range.Source files
Canonical registration in the registry. No dedicated controller — CSS for this part lives in the layered bundle.
site/registry.py · contracts/date_range.py