[]previous and next page

Form posts

form: true lets one function serve two kinds of caller: the stub, and a plain HTML <form method="post"> that works before any script has loaded.

For agentsUpdated

form: true lets one function serve two kinds of caller: the stub, and a plain HTML <form method="post"> that works before any script has loaded. For a POST whose content type is application/x-www-form-urlencoded or multipart/form-data:

  • The x-dusk-fn header and the JSON content type are not required (a browser form cannot send them). The CSRF check still applies, and a form post carries Sec-Fetch-Site and Origin.
  • The input is formToObject of the form data: a null-prototype object, repeated fields as arrays, files as File, and the keys __proto__, constructor and prototype dropped. Every value is a string or a File, so the schema converts numbers and booleans itself. A field name that is not valid UTF-8 (the parser would turn it into U+FFFD, and two different names would become one field) is a 400, as is a body that does not parse (no boundary, truncated).
  • The answer is never JSON. Success redirects with 303 to the Referer when it is a page of this origin, otherwise to the app's root. A thrown redirect() goes to its target (a 302 becomes 303, the other statuses stay). An error answers text/plain with its status and public message; validation issues are not included.
  • With security.cookieSecrets configured, the outcome of the post (the value, a fail() result, or the error with its issues) is carried to the page the browser is sent back to, in a signed one-time cookie, and that page shows it through useSubmission(). Without cookie secrets the answers above are all there is. See Dusk: mutations, forms and results.

The function's endpoint is /_dusk/fn/<id>. Both the server-side function and the browser stub expose it as .url, the same string, so a page renders the form on the server and the browser keeps it after hydration (see In the browser). The form posts without any script, which is the point of form: true:

src/routes/pin-form.tsxtsx
import type { JSX } from "@shadow-js/web";
import { pinNote } from "../server/forms";

export default function PinForm(): JSX.Element {
  return (
    <form method="post" action={pinNote.url}>
      <input type="hidden" name="id" value="1" />
      <button type="submit">Pin note 1</button>
    </form>
  );
}