Forms and mutations
A form that posts to a server function works without JavaScript and gets better with it.
Make the server function a form handler with form: true, and give it something to say when the input is wrong.
import { fail, serverFn } from "@shadow-js/dusk/server";
export const invite = serverFn({ form: true }, async (input: unknown) => {
const email =
typeof input === "object" && input !== null ? Reflect.get(input, "email") : undefined;
if (typeof email !== "string" || !email.includes("@")) {
return fail(400, { issues: [{ message: "Enter a valid email", path: ["email"] }] });
}
return { invited: email };
});
Then point a Form at it.
import { Form, useSubmission } from "@shadow-js/dusk/client";
import type { JSX } from "@shadow-js/web";
import { invite } from "../../server/invite";
export default function Invite(): JSX.Element {
const last = useSubmission(invite);
return (
<Form action={invite} resetOnSuccess>
<label>
Email <input name="email" type="email" />
</label>
<button type="submit">{last.pending ? "Sending" : "Send invite"}</button>
</Form>
);
}
Try the check the handler runs. The input below is checked in the page by the same rule, so you can see both outcomes without a server.
Show the code
import { useStore } from "@shadow-js/core";
export function InviteModel() {
const [email, setEmail] = useStore("ada@example.com");
const [sent, setSent] = useStore("");
const issue = (): string | undefined =>
email().includes("@") ? undefined : "Enter a valid email";
return (
<form
class="stack"
onSubmit={(event) => {
event.preventDefault();
setSent(issue() === undefined ? `invited: ${email()}` : "");
}}
>
<input
name="email"
aria-label="Email"
value={email()}
onInput={(event) => setEmail(event.currentTarget.value)}
/>
<button type="submit">Send invite</button>
<p>{issue() ?? sent()}</p>
</form>
);
}
Before the script loads, or with JavaScript turned off, the browser submits the form as a normal POST and the page comes back with the result. With script, the same form sends the request in place and useSubmission tells the button when it is pending. One handler serves both.
Protection comes with it#
A mutation is only accepted when it is a same-origin POST: Dusk checks the Origin and Sec-Fetch-Site headers and a request that fails either check never reaches your function.
Next: guard a folder with middleware. Field errors, optimistic updates and file uploads are in Mutations and forms.