UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

102 lines (95 loc) 3.5 kB
import type { TObject } from "alepha"; import { useAlepha } from "alepha/react"; import { useEffect, useId, useMemo, useRef } from "react"; import { type FormCtrlOptions, FormModel } from "../services/FormModel.ts"; /** * Custom hook to create a form with validation and field management. * This hook uses TypeBox schemas to define the structure and validation rules for the form. * It provides a way to handle form submission, field creation, and value management. * * @example * ```tsx * import { z } from "alepha"; * * const form = useForm({ * schema: z.object({ * username: z.text(), * password: z.text(), * }), * handler: (values) => { * console.log("Form submitted with values:", values); * }, * }); * * return ( * <form {...form.props}> * <input {...form.input.username.props} /> * <input {...form.input.password.props} /> * <button type="submit">Submit</button> * </form> * ); * ``` */ /** * Shallow / structural equality for form initial-value objects. Form values * are plain data (strings, numbers, arrays, plain objects) so JSON.stringify * is both fast enough and exact. Wrapped in try/catch to fall back to * reference equality if anything exotic sneaks in (e.g. circular refs). */ const stableEqual = (a: unknown, b: unknown): boolean => { if (a === b) return true; if (a == null || b == null) return false; try { return JSON.stringify(a) === JSON.stringify(b); } catch { return false; } }; export const useForm = <T extends TObject>( options: FormCtrlOptions<T>, deps: any[] = [], ): FormModel<T> => { const alepha = useAlepha(); const formId = useId(); const initialValuesRef = useRef(options.initialValues); const form = useMemo(() => { return alepha.inject(FormModel<T>, { lifetime: "transient", args: [options.id || formId, options], }); }, deps); // The model is memoized once, but callers' handlers close over // render-scoped state ("const [rows, setRows] = useState(...)" then // "handler: () => onNext(rows)"). Invoking the mount-time closure submits // the INITIAL state — edits silently vanish. Keep the callbacks fresh on // every commit so submit always sees the latest render's closures. useEffect(() => { Object.assign(form.options, { handler: options.handler, onError: options.onError, onChange: options.onChange, onReset: options.onReset, onCreateField: options.onCreateField, }); }); useEffect(() => { // Reference inequality alone is unsafe: callers commonly build the // `initialValues` object inline at the top of their render, which yields // a fresh reference every commit. Reinitializing on every render wipes // user-typed values via `setInitialValues` while leaving the DOM stale, // producing the "I filled the form but submit says it's empty" bug. // // Deep-equality (JSON-compare) catches the common case where the inline // literal is rebuilt with the same content. Callers who legitimately // need to swap initial values mid-edit (e.g. editing quest A → quest B) // still see a different JSON shape and trigger the reinit. if (stableEqual(initialValuesRef.current, options.initialValues)) { return; } initialValuesRef.current = options.initialValues; if (options.initialValues) { form.setInitialValues(options.initialValues as Record<string, any>); } }, [options.initialValues]); return form; };