/** * Form — binds a `useForm` instance to its fields without threading `form` * through every one of them by hand. * * `Form.Field` is a render prop, not a component that clones its child, for * the same reason `useField` is a hook and not a wrapper: PanelUI's controls * take differently-shaped change props, and only the caller knows which one * a given field needs to wire up. * * `createForm()` binds that value shape to the same runtime components * when field names and render values should stay typed through JSX. * * ```tsx * const form = useForm({ * defaultValues: { email: '' }, * onSubmit: async (values) => { ... }, * }); * *
* (value ? undefined : 'Required')}> * {(field) => ( * * )} * *
* ``` */ import { createContext, useContext, type ReactNode } from 'react'; import { useField, type FormFieldRenderProps, type UseFieldOptions } from './use-field'; import { useForm, type FormApi, type FieldErrors, type FieldTouched, type FieldState, type UseFormOptions, type Validator } from './use-form'; import { bindFormRuntime } from './typed-form'; export { useForm, useField }; export type { FormApi, FieldErrors, FieldTouched, FieldState, UseFormOptions, Validator, FormFieldRenderProps, UseFieldOptions, }; const FormContext = createContext | null>(null); export interface FormProps { form: FormApi; children?: ReactNode; } export interface TypedFormProps> { form: FormApi; children?: ReactNode; } function FormRoot({ form, children }: FormProps) { return {children}; } FormRoot.displayName = 'Form'; export interface FormFieldProps { name: string; validate?: (value: any, values: any) => string | undefined | Promise; /** Runs on blur, and always on submit. `'change'` also validates on every edit. */ validateOn?: 'blur' | 'change'; children: (field: FormFieldRenderProps) => ReactNode; } export interface TypedFormFieldProps< T extends Record, K extends keyof T, > extends UseFieldOptions { name: K; children: (field: FormFieldRenderProps) => ReactNode; } function FormField({ name, validate, validateOn, children }: FormFieldProps) { const form = useContext(FormContext); if (!form) { throw new Error('Form.Field must be rendered inside a
.'); } const field = useField(form, name, { validate, validateOn }); return <>{children(field)}; } FormField.displayName = 'Form.Field'; export const Form = Object.assign(FormRoot, { Field: FormField, }); export interface TypedForm> { (props: TypedFormProps): ReactNode; Field: (props: TypedFormFieldProps) => ReactNode; useForm: (options: UseFormOptions) => FormApi; useField: ( form: FormApi, name: K, options?: UseFieldOptions ) => FormFieldRenderProps; } /** Bind a value shape to Form's existing runtime without creating new state. */ export function createForm>(): TypedForm { return bindFormRuntime(FormRoot, FormField, useForm, useField) as TypedForm; }