Submission & Async
Basic submission
Wire a form element's onSubmit to form.handleSubmit():
import { createForm } from "@domphy/form/domphy"
const form = createForm<{ email: string; message: string }>({
defaultValues: { email: "", message: "" },
onSubmit: async ({ value }) => {
await sendMessage(value.email, value.message)
},
})
const ContactForm = {
form: [
// ... fields
{
button: "Send",
type: "submit",
disabled: (l) => !form.canSubmit(l),
},
],
onSubmit: (e: Event) => {
e.preventDefault()
form.handleSubmit()
},
}handleSubmit():
- Runs all
onSubmitvalidators across all fields - If any validator fails, sets
isSubmitted = trueand stops (errors are displayed) - If all pass, calls your
onSubmithandler - Sets
isSubmitting = trueduring the async handler - Sets
isSubmitting = falsewhen done
Async submission with loading state
const form = createForm<LoginInput>({
defaultValues: { email: "", password: "" },
onSubmit: async ({ value }) => {
const result = await loginApi(value)
router.navigate({ to: "/dashboard" })
},
})
const SubmitButton = {
button: (l) => form.isSubmitting(l) ? "Signing in…" : "Sign in",
type: "submit",
disabled: (l) => !form.canSubmit(l) || form.isSubmitting(l),
}Handling server errors
Return a rejection from onSubmit to display a server error:
const form = createForm<LoginInput>({
defaultValues: { email: "", password: "" },
onSubmit: async ({ value }) => {
const result = await loginApi(value)
if (result.error === "invalid_credentials") {
// Set a form-level error by throwing
throw new Error("Invalid email or password")
}
},
})
// Display form-level errors
const FormError = {
p: (l) => String(form.state(l).errors[0] ?? ""),
hidden: (l) => form.state(l).errors.length === 0,
style: { color: "red" },
}For field-level server errors (e.g. "email already taken"):
onSubmit: async ({ value, formApi }) => {
const result = await registerApi(value)
if (result.error === "email_taken") {
formApi.setFieldMeta("email", (meta) => ({
...meta,
errors: ["Email already in use"],
errorMap: { onSubmit: "Email already in use" },
}))
}
}Reset
Reset to default values after submission:
const form = createForm<NewItemInput>({
defaultValues: { name: "", quantity: 1 },
onSubmit: async ({ value, formApi }) => {
await addItem(value)
formApi.reset() // clear form after successful submit
},
})Reset with custom values (e.g. load next draft):
form.reset({ name: "", quantity: 1 })
form.form.reset({ name: nextDraft.name, quantity: nextDraft.qty })Multi-step forms (wizard)
Track steps with an external state — each step is a separate set of fields:
import { toState } from "@domphy/core"
const step = toState<1 | 2 | 3>(1)
const form = createForm<{
// Step 1
name: string
email: string
// Step 2
plan: "free" | "pro"
// Step 3
cardNumber: string
}>({
defaultValues: { name: "", email: "", plan: "free", cardNumber: "" },
onSubmit: async ({ value }) => {
await subscribe(value)
router.navigate({ to: "/welcome" })
},
})
const nameField = form.field<string>("name", { validators: { onSubmit: ({ value }) => value ? undefined : "Required" } })
const emailField = form.field<string>("email", {})
const planField = form.field<"free" | "pro">("plan", {})
const cardField = form.field<string>("cardNumber", {})
async function nextStep() {
const current = step.get()
if (current === 1) {
// Validate step 1 fields manually before advancing
await nameField.api.validate("submit")
await emailField.api.validate("submit")
const hasErrors = nameField.errors().length > 0 || emailField.errors().length > 0
if (!hasErrors) step.set(2)
} else if (current === 2) {
step.set(3)
} else {
form.handleSubmit()
}
}
const WizardForm = {
form: [
{ div: Step1Fields, hidden: (l) => step.get(l) !== 1 },
{ div: Step2Fields, hidden: (l) => step.get(l) !== 2 },
{ div: Step3Fields, hidden: (l) => step.get(l) !== 3 },
{
button: (l) => step.get(l) === 3 ? "Subscribe" : "Next",
type: "button",
onClick: nextStep,
disabled: (l) => form.isSubmitting(l),
},
],
onSubmit: (e) => e.preventDefault(),
}Submit with external data (context)
Pass extra data to handleSubmit that isn't in the form values:
const form = createForm<MessageInput>({
defaultValues: { body: "" },
onSubmit: async ({ value, context }) => {
await sendMessage(value.body, context.threadId, context.csrfToken)
},
})
const SendButton = {
button: "Send",
onClick: () => form.handleSubmit(
{},
{ threadId: currentThread, csrfToken: getToken() }
),
}Preventing double-submit
canSubmit automatically returns false while isSubmitting is true — wire it to the submit button's disabled:
{
button: "Save",
disabled: (l) => !form.canSubmit(l),
onClick: () => form.handleSubmit(),
}No extra debounce or lock needed — the form state handles it.