Form with progressive enhancement
<Form name="X"> renders a plain HTML form targeting /_bext/action/X. Without JS it submits normally and the server 303s back. With FORM_CLIENT_RUNTIME on the page, submissions go through fetch with x-bext-form: 1 — the server returns the action's JSON, and a bext:result CustomEvent fires on the form. Same form, two paths.
Tip
The form works without JavaScript: without the runtime the browser submits normally and the action returns a 303 redirect. FORM_CLIENT_RUNTIME inlines only ~400 B of vanilla JS and never changes the form markup — no extra data-* attributes needed.
Try it
idle
// src/actions/echoFormDemo.ts — server action returning JSON
"use server";
export async function echoFormDemo(req: Request): Promise<Response> {
const form = await req.formData();
return Response.json({ greeting: "hello, " + form.get("name"), at: new Date().toISOString() });
}
// page.tsx — Form component + client runtime
import { Form, FORM_CLIENT_RUNTIME } from "@bext-stack/framework/form";
// Same form works with or without JS:
// - no JS → standard POST 303 redirect
// - JS on → fetch with x-bext-form:1, server returns JSON, bext:result fires
<Form name="echoFormDemo">
<input name="name" required />
<button type="submit">greet</button>
</Form>
// Include the runtime once per page (inlines ~400 B of vanilla JS)
<Raw html={FORM_CLIENT_RUNTIME} />
<script>
document.addEventListener("bext:pending", e => { /* show spinner */ });
document.addEventListener("bext:result", e => {
// e.detail.result is the JSON the action returned
document.getElementById("out").textContent = e.detail.result.greeting;
});
</script>