import { type FormStoreState, useFormStore } from "@ariakit/react"; import { useState } from "react"; import { Failure, Success } from "./async-op.ts"; import { caughtValueToString } from "./caught-value.ts"; type Validator = (value: T) => string | null; type MaybePromise = T | Promise; export function useForm(props: { defaultValues: T; validate?: { [K in keyof T]?: Validator }; /** * Handles form submission login. * * This function should return a Success or Failure. Failures should always contain a string * which will be used as the form error message. */ onSubmit: ( state: FormStoreState, ) => MaybePromise | Failure>; /** * If submission was successful (i.e. onSubmit returned a Success), will be run to perform any * side-effect necessary. * * Typically this is used for navigation on mutating some local state. */ onSuccess?: (value: R, state: FormStoreState) => MaybePromise; }) { const submitName = "submit"; const [op, setOp] = useState > | null>(null); const form = useFormStore({ defaultValues: props.defaultValues, }); form.useSubmit(async (state) => { try { const submitOp = await props.onSubmit(state); if (submitOp.isFailure) { form.setError(submitName, submitOp.failure); } if (submitOp.isSuccess) { await props.onSuccess?.(submitOp.value, state); } setOp(submitOp); } catch (error) { form.setError(submitName, caughtValueToString(error)); } }); form.useValidate((state) => { if (props.validate) { const entries = Object.entries(props.validate) as Array< [keyof T & string, Validator] >; for (const [key, validate] of entries) { const value = state.values[key]; const message = validate(value); if (message) { form.setError(key, message); } } } }); return { form, submitName, op }; }