Skip to content

Extend the HTMX scaffold

FastAPI scaffold

This page extends @app.view from Build your first app. Flask and Django adapters keep their own HedronBlueprint / @hedron_view APIs — do not paste this onto an adapter scaffold.

You already have a working Refresh loop from Build your first app. This page extends that same app.py — do not paste a second full scaffold.

Default vs explicit APIs: Which interaction API?. 0.50 authoring (ActionHandle.effect / .after(load=), Select.depends_on, Lazy error templates, danger Toast dismiss) is documented on Interaction API.

What you already have

hedron new registered:

  • @app.view("/status") as status
  • status() on the home page
  • status.refresh_button("Refresh status")

Click Refresh status once to confirm the UTC timestamp updates. If that works, the generated /status route is correct — continue below.

60-second mental model

  1. status.refresh_button(...) emits hx-get="/status", hx-target, and hx-swap.
  2. The browser requests /status with HTMX headers (HX-Request, HX-Target).
  3. The server returns only the region HTML, not a full document.
  4. HTMX swaps that HTML into the target.

A wrong HX-Target returns 403 by design.

Try it (simulated)

Two refreshable regions on the Hello scaffold — docs simulation (no live server).

Service healthyChecked at __HEDRON_SIM_LOCAL_TIME__
Sample notes regionAllowlisted #notes-count — count stays 0 in this sim
Allowlist probeTry the correct vs wrong target buttons.

Click a control to simulate an HTMX fragment request.

Minimal runnable app.py that reproduces this demo (real Hedron, not the docs simulator):

app.py
import os
from datetime import datetime, timezone

from hedron import Hedron, Page, Stack, Text, html

app = Hedron(
    title="HTMX interactions",
    security="standard",
    explorer="off",
    session_secret=os.environ.get("HEDRON_SESSION_SECRET", "dev-only"),
)

_NOTES: list[str] = []


@app.view("/status")
def status():
    stamp = datetime.now(timezone.utc).strftime("%H:%M:%S")
    return html.div(
        Text(f"All systems operational · refreshed {stamp}"),
        role="status",
        aria={"live": "polite"},
    )


@app.view("/notes-count")
def notes():
    return html.div(
        Text(f"Notes saved: {len(_NOTES)}"),
        role="status",
        aria={"live": "polite"},
    )


@app.page("/")
def home() -> Page:
    return Page(
        Stack(
            Text("Hello from hedron new"),
            status(),
            status.refresh_button("Refresh status"),
            notes(),
            notes.refresh_button("Refresh notes count"),
        ),
        title="Home",
    )

Delta: add a second refreshable view

Keep the existing status view. Add a second refreshable that counts notes in memory.

1. Add a notes view (below your existing status function)

_NOTES: list[str] = []


@app.view("/notes-count")
def notes():
    return html.div(
        Text(f"Notes saved: {len(_NOTES)}"),
        role="status",
        aria={"live": "polite"},
    )

2. Extend home() — keep the greeting and status controls

@app.page("/")
def home() -> Page:
    return Page(
        Stack(
            Text("Hello from hedron new"),
            status(),
            status.refresh_button("Refresh status"),
            notes(),
            notes.refresh_button("Refresh notes count"),
        ),
        title="Home",
    )

Reload the app, click Refresh notes count. The count stays at 0 until you add a form in Minimal form POST that appends to _NOTES (same scaffold).

Stuck with 403? The HX-Target did not match the view’s host. See Troubleshooting.

Contracts (after the second click)

Contract Responsibility
@app.view("/path") Registers a GET fragment view and returns a handle.
status() Renders the view (and its host) on the page.
status.refresh_button(...) Wires hx-get / hx-target / hx-swap from that handle.

Explicit allowlist (FragmentRegion)

hedron new does not generate explicit selector allowlists. FragmentRegion remains an Advanced API when you need one for a custom route. See Which interaction API? and Interaction.

Flask / Django

Wire the same Refresh pattern through hedron_route / hedron_view and interaction_response — see Flask and Django. Prefer polling for job status; FastAPI-only SSE/WebSocket helpers are experimental.

Inspect / test

curl \
  -H 'HX-Request: true' \
  -H 'HX-Target: #h-view-notes' \
  http://127.0.0.1:8000/notes-count
test_app.py
from fastapi.testclient import TestClient

from app import app


def test_notes_count_fragment() -> None:
    with TestClient(app) as client:
        response = client.get(
            "/notes-count",
            headers={"HX-Request": "true", "HX-Target": "#h-view-notes"},
        )
    assert response.status_code == 200
    assert "Notes saved:" in response.text
    assert "<html" not in response.text


def test_status_rejects_an_unknown_target() -> None:
    with TestClient(app) as client:
        response = client.get(
            "/status",
            headers={"HX-Request": "true", "HX-Target": "#other-panel"},
        )
    assert response.status_code == 403

Confirm the host id in the page HTML if #h-view-notes does not match your build.

When the interaction mutates state

Use @app.action for a mutation. Built-in security profiles validate CSRF on unsafe methods automatically after a safe GET seeds the cookie.

Next: Minimal form POST — add a POST that appends to _NOTES (same scaffold).

Also: Which interaction API? · Security · Test your UI · Interaction API · Hedron API