// ── @zerotal/flow plain-form enhancement bundle ───────────────────────────────── // // The entry point for the second Bun.build() in FlowProvider.onBooting(). It // produces /__flow/enhance.js. // // **Deliberately separate from the runtime bundle, and deliberately tiny.** The // whole point of this feature is a page with *no Flow component on it* — an // ordinary server-rendered form, on a route that is not `Router.flow`. Such a page // is never served `/__flow/runtime.js`, so there is no script to hook into; and // pulling Alpine and the WebSocket bridge onto a page with no component to drive // would be paying the entire framework's client cost for a submit handler. // // **This file exports nothing, and must not.** It is loaded as a classic script, // where a trailing `export` statement is a SyntaxError that discards the whole // bundle — silently, because the forms then post natively and the page still // works. The decisions worth testing live in `enhanceSupport.ts`, which Bun inlines // back in at build time. // // ## The contract // //
// // The form posts through `fetch`, and the matching form in the response replaces // this one in place. Validation errors then appear where they belong and the rest // of the page is untouched — no flash, no scroll loss, no re-render of a page that // did not change. // // Everything about it degrades: // // - **No JavaScript is an ordinary post.** `data-enhance` is additive on a form // that already has `method` and `action`, so the no-JS path is not a fallback // that was written and might rot — it is the form itself, unmodified. If that // is ever not free, the word "enhancement" is doing no work. // - **A network failure re-submits natively.** The difference between a bad // connection being slow and a bad connection losing what somebody typed. // - **A redirect is followed**, its document swapped in and `pushState`d, so the // address bar agrees with the page. import { ATTR, BUSY_ATTR, matchIn, sameUrl, targetFor } from "./enhanceSupport.ts"; /** * Move focus back to where it was before the swap. * * Replacing a subtree destroys the focused element, and a form that reports "this * field is required" having just taken the cursor away from that field is worse * than one that did not enhance at all. Matched by `name`, which survives a * re-render in a way a DOM node cannot. */ function restoreFocus(root: Element, name: string | null, start: number | null): void { if (!name) return; const next = root.querySelector(`[name="${CSS.escape(name)}"]`); if (!next) return; next.focus(); if (start === null) return; try { next.setSelectionRange(start, start); } catch { // Not a text-like control (checkbox, select, file) — focus alone is the job. } } /** * Submit the form the way the browser would have, with the enhancement out of the * way so this handler does not catch its own fallback. */ function nativeSubmit(form: HTMLFormElement, submitter: HTMLElement | null): void { form.removeAttribute(ATTR); if (submitter && "click" in submitter) { // Through the submitter, so a named button still contributes its value. submitter.click(); return; } form.submit(); } /** * Handle one enhanced submission. * * Throws nothing: every failure path either falls back to a native submit or * leaves the page exactly as it was. */ async function submitEnhanced(form: HTMLFormElement, submitter: HTMLElement | null): Promise { const method = (form.getAttribute("method") || "get").toUpperCase(); const action = form.getAttribute("action") || window.location.href; // `new FormData(form, submitter)` includes the button that was actually pressed, // which is how `