UNPKG

@conform-to/react

Version:

Conform view adapter for react

854 lines 39.6 kB
import type { CustomSerialize, FieldName, FormError, FormValue, Serialize, SubmissionResult, ValidationAttributes } from '@conform-to/dom/future'; import type { StandardSchemaV1 } from './standard-schema'; export type Prettify<T> = { [K in keyof T]: T[K]; } & {}; /** Reference to a form element. Can be either a React ref object or a form ID string. */ export type FormRef = React.RefObject<HTMLFormElement | HTMLFieldSetElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | null> | string; export type DefaultControlValue = string | string[] | File | File[] | FileList; export type Control<Value = DefaultControlValue, DefaultValue = Value, Payload = unknown> = { /** * Current string value derived from the control payload. */ value: string | undefined; /** * Checked state derived from the control payload. */ checked: boolean | undefined; /** * Current string array derived from the control payload. */ options: string[] | undefined; /** * Current file array derived from the control payload. */ files: File[] | undefined; /** * The rendered payload used as the source for base control(s). * * For simple native controls, this mirrors `defaultValue` / `defaultChecked`. * For structural controls (i.e. `<fieldset>`), this is the latest payload * snapshot that drives which hidden inputs are rendered. */ defaultValue: DefaultValue | null | undefined; /** * Current payload snapshot derived from the registered base control(s). * * For structural controls (i.e. `<fieldset>`), this is reconstructed from * descendant fields under the registered fieldset name. */ payload: Payload | null | undefined; /** * Registers the base control element. * * Accepts `<input>`, `<select>`, `<textarea>`, `<fieldset>`, * or a collection of checkbox / radio inputs with the same name. */ register: (element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFieldSetElement | HTMLCollectionOf<HTMLInputElement> | NodeListOf<HTMLInputElement> | null | undefined) => void; /** * A ref object containing the form element associated with the registered base control. * Use this with hooks like useFormData() and useIntent(). */ formRef: React.RefObject<HTMLFormElement | null>; /** * Programmatically updates the input value and emits * both [change](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) and * [input](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event) events. */ change: (value: Value | null) => void; /** * Emits [focus](https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event) and * [focusin](https://developer.mozilla.org/en-US/docs/Web/API/Element/focusin_event) events. * * This does not move the actual keyboard focus to the input. * Use [HTMLElement.focus()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) * if you want to move focus to the input. */ focus: () => void; /** * Emits [blur](https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event) and * [focusout](https://developer.mozilla.org/en-US/docs/Web/API/Element/focusout_event) events. * * This does not move the actual keyboard focus away from the input. */ blur: () => void; }; export type StandardControlOptions<Value extends DefaultControlValue = DefaultControlValue> = { /** * The initial value of the base control. */ defaultValue?: Value | null | undefined; /** * A callback function that is triggered when the base control is focused. * Use this to delegate focus to a custom input. */ onFocus?: () => void; }; export type CheckedControlOptions = { /** * Whether the base control should be checked by default. */ defaultChecked?: boolean | undefined; /** * The value of a checkbox or radio control when checked. */ value?: string; /** * A callback function that is triggered when the base control is focused. * Use this to delegate focus to a custom input. */ onFocus?: () => void; }; export type CustomControlOptions<Value = unknown, DefaultValue = Value> = { /** * Initial value used to seed the control. * For structural controls, this is the payload used to render hidden inputs. */ defaultValue?: DefaultValue | null | undefined; /** * Payload parser applied to the current payload snapshot. * * Use this to coerce unknown DOM-derived data into a typed shape. * Any thrown error is surfaced to the caller. */ parse: (payload: unknown) => Value | null; /** * Optional serializer to convert the parsed payload back to a form value for populating the base control(s). */ serialize?: (value: Value) => FormValue; /** * A callback function that is triggered when the base control is focused. * Use this to delegate focus to a custom input. */ onFocus?: () => void; }; export type ControlOptions = StandardControlOptions | CheckedControlOptions | CustomControlOptions; export type Selector<FormValue, Result> = (formData: FormValue, lastResult: Result | undefined) => Result; export type UseFormDataOptions<Value = undefined> = { /** * Set to `true` to preserve file inputs and receive a `FormData` object in the selector. * If omitted or `false`, the selector receives a `URLSearchParams` object, where all values are coerced to strings. */ acceptFiles?: boolean; /** * The fallback value to return when the form element is not available (e.g., on SSR or initial client render). * If not provided, the hook returns `undefined` when the form is unavailable. */ fallback?: Value; }; export type DefaultValue<Shape> = Shape extends Record<string, any> ? { [Key in keyof Shape]?: DefaultValue<Shape[Key]>; } | null | undefined : Shape extends Array<infer Item> ? Array<DefaultValue<Item>> | null | undefined : Shape extends File | File[] ? null | undefined : Shape | string | null | undefined; export type FormState<ErrorShape = any> = { /** Unique identifier that changes on form reset to trigger reset side effects */ resetKey: string; /** Initial form values */ defaultValue: Record<string, unknown>; /** Form values that will be synced to the DOM */ targetValue: Record<string, unknown> | null; /** Form values from server actions, or submitted values when no server intent exists */ serverValue: Record<string, unknown> | null; /** Validation errors from server-side processing */ serverError: FormError<ErrorShape> | null; /** Validation errors from client-side validation */ clientError: FormError<ErrorShape> | null; /** Array of field names that have been touched (validated) */ touchedFields: string[]; /** Mapping of array field names to their item keys for React list rendering */ listKeys: Record<string, string[]>; }; export type FormAction<ErrorShape = any, Intent extends UnknownIntent | null | undefined = UnknownIntent | null, Context = {}> = SubmissionResult<ErrorShape> & { type: 'initialize' | 'server' | 'client'; intent: Intent; ctx: Context; }; /** * Augment this interface to customize schema type inference for your schema library. * * **Example:** * ```ts * import type { ZodTypeAny, input, output } from 'zod'; * import type { ZodSchemaOptions } from '@conform-to/zod/v3/future'; * * declare module '@conform-to/react/future' { * interface CustomSchemaTypes<Schema> { * input: Schema extends ZodTypeAny ? input<Schema> : never; * output: Schema extends ZodTypeAny ? output<Schema> : never; * options: Schema extends ZodTypeAny ? ZodSchemaOptions : never; * } * } * ``` */ export interface CustomSchemaTypes<Schema = unknown> { } /** * Infer schema options type. * Uses CustomSchemaTypes if augmented, otherwise returns never. */ export type InferOptions<Schema> = [Schema] extends [undefined] ? never : CustomSchemaTypes<Schema> extends { options: infer T; } ? T : never; /** * Marker type for conditional field metadata properties. * Used to indicate that a property should only be present when FieldShape matches a condition. */ export type ConditionalFieldMetadata<T, Condition> = T & { '~condition': Condition; }; /** * Check if T is a FieldName type by checking if '~shape' is a known key. * Plain strings don't have '~shape' in their keyof, but FieldName<T> does. */ type IsFieldName<T> = '~shape' extends keyof T ? true : false; /** * Transforms a single value, restoring FieldName<unknown> to FieldName<FieldShape>. */ type RestoreFieldShapeValue<T, FieldShape> = T extends ConditionalFieldMetadata<infer Inner, infer Condition> ? FieldShape extends Condition ? Inner : never : IsFieldName<T> extends true ? FieldName<FieldShape> : T; /** * Restores FieldShape-dependent types that were inferred as `unknown`. * This is needed because TypeScript infers generic return types with unresolved type parameters. * Also handles conditional field metadata by checking if FieldShape extends the condition. * * Transforms up to 2 levels deep to handle cases like `textFieldProps.name`. */ export type RestoreFieldShape<T, FieldShape> = { [K in keyof T]: RestoreFieldShapeValue<T[K], FieldShape> extends infer V ? V extends Record<string, unknown> ? { [P in keyof V]: RestoreFieldShapeValue<V[P], FieldShape>; } : V : never; }; /** * Type guard function for conditional field metadata. * Used to specify when a custom field metadata property should be available. */ export type FieldShapeGuard<Condition> = (shape: unknown) => shape is Condition; /** * Function type for creating conditional field metadata based on shape constraints. */ export type DefineConditionalField = <FieldShape, ErrorShape, Metadata>(metadata: BaseFieldMetadata<unknown, ErrorShape>, shape: FieldShapeGuard<FieldShape>, fn: (metadata: BaseFieldMetadata<FieldShape, ErrorShape>) => Metadata) => ConditionalFieldMetadata<Metadata, FieldShape>; /** * Extract the condition type from a FieldShapeGuard. */ export type ExtractFieldCondition<T> = T extends FieldShapeGuard<infer C> ? C : never; /** * Extract conditions from a record of field shape guards. */ export type ExtractFieldConditions<T extends Record<string, FieldShapeGuard<any>>> = { [K in keyof T]: ExtractFieldCondition<T[K]>; }; /** * Resolved configuration from configureForms factory. * Properties with defaults are required, others remain optional. */ export type FormsConfig<BaseErrorShape, BaseSchema, SchemaErrorShape, CustomFormMetadata extends Record<string, unknown>, CustomFieldMetadata extends Record<string, unknown>> = { /** * The name of the submit button field that indicates the submission intent. * @default "__intent__" */ intentName: string; /** * A custom serializer for converting form values. */ serialize?: CustomSerialize | undefined; /** * Determines when validation should run for the first time on a field. * @default "onSubmit" */ shouldValidate: 'onSubmit' | 'onBlur' | 'onInput'; /** * Determines when validation should run again after the field has been validated once. * @default Same as shouldValidate */ shouldRevalidate: 'onSubmit' | 'onBlur' | 'onInput'; /** * Runtime type guard to check if a value is a schema. * Used to determine if the first argument to useForm is a schema or options object. * * **Example:** * ```ts * import { configureForms } from '@conform-to/react/future'; * import { * isSchema, * validateSchema, * getConstraints, * } from '@conform-to/zod/v3/future'; * * const { useForm } = configureForms({ * isSchema, * validateSchema, * getConstraints, * }); * ``` */ isSchema: (schema: unknown) => schema is BaseSchema; /** * Validates a schema against form payload. */ validateSchema: <Schema extends BaseSchema>(schema: Schema, payload: Record<string, FormValue>, options?: InferOptions<Schema>) => MaybePromise<{ error: FormError<SchemaErrorShape> | null; value?: InferOutput<Schema>; }>; /** * A type guard function to specify the shape of error objects. */ isError?: (error: unknown) => error is BaseErrorShape; /** * Extracts HTML validation constraints from a schema. */ getConstraints?: <Schema extends BaseSchema>(schema: Schema) => Record<string, ValidationAttributes> | undefined; /** * Extends form metadata with custom properties. */ extendFormMetadata?: <ErrorShape extends BaseErrorShape>(metadata: BaseFormMetadata<ErrorShape>) => CustomFormMetadata; /** * Extends field metadata with custom properties. * Use `when` for properties that depend on the field shape. */ extendFieldMetadata?: <FieldShape, ErrorShape extends BaseErrorShape>(metadata: BaseFieldMetadata<FieldShape, ErrorShape>, ctx: { form: BaseFormMetadata<ErrorShape>; when: DefineConditionalField; }) => CustomFieldMetadata; }; export type NonPartial<T> = { [K in keyof Required<T>]: T[K]; }; export type RequireKey<T, K extends keyof T> = Prettify<Omit<T, K> & { [P in K]-?: Exclude<T[P], undefined>; }>; export type BaseSchemaType = StandardSchemaV1<any, any>; /** * Infer schema input type. * For StandardSchemaV1 schemas (zod, valibot, etc.), uses StandardSchemaV1.InferInput. * For other schemas, uses CustomSchemaTypes if augmented. */ export type InferInput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema> : CustomSchemaTypes<Schema> extends { input: infer T; } ? T : Record<string, any>; export type InferFormShape<Schema> = InferInput<Schema> extends Record<string, any> ? InferInput<Schema> : Record<string, any>; /** * Infer schema output type. * For StandardSchemaV1 schemas (zod, valibot, etc.), uses StandardSchemaV1.InferOutput. * For other schemas, uses CustomSchemaTypes if augmented. */ export type InferOutput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<Schema> : CustomSchemaTypes<Schema> extends { output: infer T; } ? T : undefined; export type FormOptions<FormShape extends Record<string, any> = Record<string, any>, ErrorShape = any, Value = undefined, Schema = unknown, SchemaErrorShape = ErrorShape> = { /** Optional form identifier. If not provided, a unique ID is automatically generated. */ id?: string | undefined; /** Optional key for form state reset. When the key changes, the form resets to its initial state. */ key?: string | undefined; /** Server-side submission result for form state synchronization. */ lastResult?: SubmissionResult<ErrorShape> | null | undefined; /** Form submission handler called when the form is submitted with no validation errors. */ onSubmit?: SubmitHandler<FormShape, NoInfer<ErrorShape>, NoInfer<Value>> | undefined; /** Initial form values. Can be a partial object matching your form structure. */ defaultValue?: DefaultValue<FormShape> | undefined; /** * Override serialization for specific fields on this form and delegate the rest * to the configured global serializer. */ serialize?: CustomSerialize | undefined; /** HTML validation attributes for fields (required, minLength, pattern, etc.). */ constraint?: Record<string, ValidationAttributes> | undefined; /** * Schema-specific validation options (e.g., Zod's errorMap). * The available options depend on the schema library configured in `configureForms`. */ schemaOptions?: InferOptions<Schema>; /** * Determines when validation should run for the first time on a field. * Overrides the default configured through `configureForms()` if provided. * * @default Inherits from `configureForms()`, or "onSubmit" if not configured */ shouldValidate?: 'onSubmit' | 'onBlur' | 'onInput' | undefined; /** * Determines when validation should run again after the field has been validated once. * Overrides the default configured through `configureForms()` if provided. * * @default Inherits from `configureForms()`, or same as shouldValidate */ shouldRevalidate?: 'onSubmit' | 'onBlur' | 'onInput' | undefined; /** Error handling callback triggered when validation errors occur. By default, it focuses the first invalid field. */ onError?: ErrorHandler<NoInfer<ErrorShape>> | undefined; /** Input event handler for custom input event logic. */ onInput?: InputHandler | undefined; /** Blur event handler for custom focus handling logic. */ onBlur?: BlurHandler | undefined; /** Custom validation handler. Can be skipped when a schema is passed as the first argument, or combined with schema validation to customize errors. */ onValidate?: ValidateHandler<ErrorShape, Value, InferOutput<Schema>, SchemaErrorShape> | undefined; }; /** * The object returned by `useForm()`, containing form-level metadata, * typed field metadata, and the intent dispatcher for programmatic updates. */ export type FormHandle<FormShape extends Record<string, any>, ErrorShape, CustomFormMetadata extends Record<string, unknown> = {}, CustomFieldMetadata extends Record<string, unknown> = {}> = { /** Form-level metadata and helpers. */ form: FormMetadata<ErrorShape, CustomFormMetadata, CustomFieldMetadata>; /** Field metadata mapped from the form shape. */ fields: Fieldset<FormShape, ErrorShape, CustomFieldMetadata>; /** Intent dispatcher for validate, reset, insert, remove, reorder, and update actions. */ intent: IntentDispatcher<FormShape>; }; export interface FormContext<ErrorShape = any> { /** The form's unique identifier */ formId: string; /** Internal form state with validation results and field data */ state: FormState<ErrorShape>; /** Serializer used to derive field defaults and sync values for this form. */ serialize: Serialize; /** HTML validation attributes for fields */ constraint: Record<string, ValidationAttributes> | null; /** Form submission event handler */ handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void; /** Input event handler for form-wide input events */ handleInput: (event: React.FormEvent) => void; /** Blur event handler for form-wide blur events */ handleBlur: (event: React.FocusEvent) => void; } export type UnknownIntent = { type: string; payload?: unknown; }; export type UnknownArgs<Args extends any[]> = { [Key in keyof Args]: unknown; }; export interface IntentDispatcher<FormShape extends Record<string, any> = Record<string, any>> { /** * Validate the whole form or a specific field? */ validate(name?: string): void; /** * Reset the form to a specific default value. * * @param options.defaultValue - The value to reset the form to. Pass `null` to clear all fields, or omit to reset to the initial default value from `useForm`. * * **Example:** * ```tsx * // Reset to initial default value * intent.reset() * * // Clear all fields * intent.reset({ defaultValue: null }) * * // Restore to a specific snapshot * intent.reset({ defaultValue: snapshotValue }) * ``` */ reset(options?: { /** * The value to reset the form to. If not provided, resets to the default value from `useForm`. Pass `null` to clear all fields instead. */ defaultValue?: DefaultValue<FormShape>; }): void; /** * Update a field or a fieldset. * If you provide a fieldset name, it will update all fields within that fieldset */ update<FieldShape = FormShape>(options: { /** * The name of the field. If you provide a fieldset name, it will update all fields within that fieldset. */ name?: FieldName<FieldShape>; /** * Specify the index of the item to update if the field is an array. */ index?: undefined; /** * The new value for the field or fieldset. */ value: DefaultValue<FieldShape>; } | { /** * The name of the field. If you provide a fieldset name, it will update all fields within that fieldset. */ name: FieldName<FieldShape>; /** * Specify the index of the item to update if the field is an array. */ index: number; /** * The new value for the field or fieldset. * When index is specified, this should be the item type, not the array type. */ value: unknown extends FieldShape ? any : FieldShape extends Array<infer ItemShape> ? ItemShape : any; }): void; /** * Insert a new item into an array field. */ insert<FieldShape extends Array<any> | null | undefined>(options: { /** * The name of the array field to insert into. */ name: FieldName<FieldShape>; /** * The index at which to insert the new item. * If not provided, it will be added to the end of the array. */ index?: number; /** * The default value for the new item. */ defaultValue?: NonNullable<FieldShape> extends Array<infer ItemShape> ? DefaultValue<ItemShape> : never; /** * The name of a field to read the value from. * When specified, the value is read from this field, validated, * and if valid, inserted into the array and the source field is cleared. * If validation fails, the error is shown on the source field instead. * Requires the validation error to be available synchronously. */ from?: string; /** * What to do when the insert causes a validation error on the array. * - 'revert': Don't insert, keep original array state. * Requires the validation error to be available synchronously. */ onInvalid?: 'revert'; }): void; /** * Remove an item from an array field. */ remove<FieldShape extends Array<any> | null | undefined>(options: { /** * The name of the array field to remove from. */ name: FieldName<FieldShape>; /** * The index of the item to remove. */ index: number; /** * What to do when the remove causes a validation error on the array. * - 'revert': Don't remove, keep original item as-is. * - 'insert': Remove the item but insert a new blank item at the end. * Requires the validation error to be available synchronously. */ onInvalid?: 'revert' | 'insert'; /** * The default value for the new item when onInvalid is 'insert'. */ defaultValue?: NonNullable<FieldShape> extends Array<infer ItemShape> ? DefaultValue<ItemShape> : never; }): void; /** * Reorder items in an array field. */ reorder(options: { name: FieldName<Array<any>>; from: number; to: number; }): void; } export type FormIntent<Dispatcher extends IntentDispatcher = IntentDispatcher> = { [Type in keyof Dispatcher]: Dispatcher[Type] extends (...args: infer Args) => void ? { type: Type; payload: Args extends [infer Payload] ? Payload : undefined; } : never; }[keyof Dispatcher]; export type IntentHandler<Signature extends (payload: any) => void = (payload: any) => void> = { validate?(...args: UnknownArgs<Parameters<Signature>>): boolean; resolve?(value: Record<string, FormValue>, ...args: Parameters<Signature>): Record<string, FormValue> | undefined; apply?<ErrorShape>(result: SubmissionResult<ErrorShape>, ...args: Parameters<Signature>): SubmissionResult<ErrorShape>; update?<ErrorShape>(state: FormState<ErrorShape>, action: FormAction<ErrorShape, { type: string; payload: Signature extends (payload: infer Payload) => void ? Payload : undefined; }, { reset: (defaultValue?: Record<string, unknown> | null) => FormState<ErrorShape>; cancelled?: boolean; }>): FormState<ErrorShape>; }; type BaseCombine<T, K extends PropertyKey = T extends unknown ? keyof T : never> = T extends unknown ? T & Partial<Record<Exclude<K, keyof T>, never>> : never; export type Combine<T> = { [K in keyof BaseCombine<T>]: BaseCombine<T>[K]; }; export type SatisfyComponentProps<ElementType extends React.ElementType, CustomProps extends React.ComponentPropsWithoutRef<ElementType>> = CustomProps; /** Field metadata object containing field state, validation attributes, and nested field access methods. */ export type FieldMetadata<FieldShape, ErrorShape = any, CustomFieldMetadata extends Record<string, unknown> = {}> = Readonly<Prettify<ValidationAttributes & { /** Unique key for React list rendering (for array fields). */ key: string | undefined; /** The field name path exactly as provided. */ name: FieldName<FieldShape>; /** The field's unique identifier, automatically generated as {formId}-field-{fieldName}. */ id: string; /** Auto-generated ID for associating field descriptions via aria-describedby. */ descriptionId: string; /** Auto-generated ID for associating field errors via aria-describedby. */ errorId: string; /** The form's unique identifier for associating field via the `form` attribute. */ formId: string; /** * The field's default value as a string. * * Returns an empty string `''` when: * - No default value is set (field value is `null` or `undefined`) * - The field value cannot be serialized to a string (e.g., objects or arrays) */ defaultValue: string; /** * Default selected options for multi-select fields or checkbox group. * * Returns an empty array `[]` when: * - No default options are set (field value is `null` or `undefined`) * - The field value cannot be serialized to a string array (e.g., nested objects or arrays of objects) */ defaultOptions: string[]; /** * Default checked state for checkbox inputs. Returns `true` if the field value is `'on'`. * * For radio buttons, compare the field's `defaultValue` with the radio button's value attribute instead. */ defaultChecked: boolean; /** * The normalized default payload at this field path. * * This is useful for non-native field shapes that need to render a set of * hidden inputs before user interaction. */ defaultPayload: unknown; /** Whether this field has been touched (through intent.validate() or the shouldValidate option). */ touched: boolean; /** Whether this field currently has no validation errors. */ valid: boolean; /** @deprecated Use `.valid` instead. This was not an intentionl breaking change and would be removed in the next minor version soon */ invalid: boolean; /** Validation error for this field. */ errors: ErrorShape | undefined; /** Object containing validation errors for all touched subfields. */ fieldErrors: Record<string, ErrorShape>; /** Boolean value for the `aria-invalid` attribute. Indicates whether the field has validation errors for screen readers. */ ariaInvalid: boolean | undefined; /** String value for the `aria-describedby` attribute. Contains the errorId when invalid, undefined otherwise. Merge with descriptionId manually if needed (e.g. `${metadata.descriptionId} ${metadata.ariaDescribedBy}`). */ ariaDescribedBy: string | undefined; /** Method to get nested fieldset for object fields under this field. */ getFieldset<FieldsetShape = keyof NonNullable<FieldShape> extends never ? unknown : FieldShape>(): Fieldset<FieldsetShape, ErrorShape, CustomFieldMetadata>; /** Method to get array of fields for list/array fields under this field. */ getFieldList<FieldItemShape = [FieldShape] extends [ Array<infer ItemShape> | null | undefined ] ? ItemShape : unknown>(): Array<FieldMetadata<FieldItemShape, ErrorShape, CustomFieldMetadata>>; } & RestoreFieldShape<CustomFieldMetadata, FieldShape>>>; /** * Field metadata without custom extensions. This is the type received in `extendFieldMetadata`. * Equivalent to `FieldMetadata<FieldShape, ErrorShape, {}>`. */ export type BaseFieldMetadata<FieldShape, ErrorShape = any> = FieldMetadata<FieldShape, ErrorShape, {}>; /** Fieldset object containing all form fields as properties with their respective field metadata. */ export type Fieldset<FieldShape, ErrorShape = any, CustomFieldMetadata extends Record<string, unknown> = {}> = { [Key in keyof Combine<FieldShape>]-?: FieldMetadata<Combine<FieldShape>[Key], ErrorShape, CustomFieldMetadata>; }; /** Form-level metadata and state object containing validation status, errors, and field access methods. */ export type FormMetadata<ErrorShape = any, CustomFormMetadata extends Record<string, unknown> = {}, CustomFieldMetadata extends Record<string, unknown> = {}> = Readonly<{ /** Unique identifier that changes on form reset */ key: string; /** The form's unique identifier. */ id: string; /** Auto-generated ID for associating form descriptions via aria-describedby. */ descriptionId: string; /** Auto-generated ID for associating form errors via aria-describedby. */ errorId: string; /** Whether any field in the form has been touched (through intent.validate() or the shouldValidate option). */ touched: boolean; /** Whether the form currently has no validation errors. */ valid: boolean; /** @deprecated Use `.valid` instead. This was not an intentional breaking change and would be removed in the next minor version soon */ invalid: boolean; /** Form-level validation error, if any exists. */ errors: ErrorShape | undefined; /** Object containing validation errors for all touched fields. */ fieldErrors: Record<string, ErrorShape>; /** The form's initial default values. */ defaultValue: Record<string, unknown>; /** Form props object for spreading onto the <form> element. */ props: Readonly<{ id: string; onSubmit: React.FormEventHandler<HTMLFormElement>; onBlur: React.FocusEventHandler<HTMLFormElement>; onInput: React.FormEventHandler<HTMLFormElement>; noValidate: boolean; }>; /** The current state of the form */ context: FormContext<ErrorShape>; /** Method to get metadata for a specific field by name. */ getField<FieldShape>(name: FieldName<FieldShape>): FieldMetadata<FieldShape, ErrorShape, CustomFieldMetadata>; /** Method to get a fieldset object for nested object fields. */ getFieldset<FieldShape>(name?: FieldName<FieldShape>): Fieldset<keyof NonNullable<FieldShape> extends never ? unknown : FieldShape, ErrorShape, CustomFieldMetadata>; /** Method to get an array of field objects for array fields. */ getFieldList<FieldShape>(name: FieldName<FieldShape>): Array<FieldMetadata<[ FieldShape ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, ErrorShape, CustomFieldMetadata>>; } & CustomFormMetadata>; /** * Form metadata without custom extensions. This is the type received in `extendFormMetadata`. * Equivalent to `FormMetadata<ErrorShape, {}, CustomFieldMetadata>`. */ export type BaseFormMetadata<ErrorShape = any, CustomFieldMetadata extends Record<string, unknown> = {}> = FormMetadata<ErrorShape, {}, CustomFieldMetadata>; export type ValidateResult<ErrorShape, Value> = FormError<ErrorShape> | null | { error: FormError<ErrorShape> | null; value?: Value; }; export type ValidateContext<SchemaValue, SchemaErrorShape> = { /** * The submitted values mapped by field name. * Supports nested names like `user.email` and indexed names like `items[0].id`. */ payload: Record<string, FormValue>; /** * Form error object. Initially empty, but populated with schema validation * errors when a schema is provided and validation fails. */ error: FormError<SchemaErrorShape>; /** * The submission intent derived from the button that triggered the form submission. */ intent: UnknownIntent | null; /** * The raw FormData object of the submission. */ formData: FormData; /** * Reference to the HTML form element that triggered the submission. */ formElement: HTMLFormElement; /** * The specific element (button/input) that triggered the form submission. */ submitter: HTMLElement | null; /** * The validated value from schema validation. Only defined when a schema is provided * and the validation succeeds. Undefined if no schema is provided or validation fails. */ schemaValue: SchemaValue; }; export type ValidateHandler<ErrorShape, Value, SchemaValue = undefined, SchemaErrorShape = ErrorShape> = (ctx: ValidateContext<SchemaValue, SchemaErrorShape>) => ValidateResult<ErrorShape, Value> | Promise<ValidateResult<ErrorShape, Value>> | [ ValidateResult<ErrorShape, Value> | undefined, Promise<ValidateResult<ErrorShape, Value>> | undefined ] | undefined; export interface FormInputEvent extends React.FormEvent<HTMLFormElement> { currentTarget: EventTarget & HTMLFormElement; target: EventTarget & (HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFieldSetElement); } export interface FormFocusEvent extends React.FormEvent<HTMLFormElement> { currentTarget: EventTarget & HTMLFormElement; relatedTarget: EventTarget | null; target: EventTarget & (HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFieldSetElement); } export type ErrorContext<ErrorShape> = { formElement: HTMLFormElement; error: FormError<ErrorShape>; intent: UnknownIntent | null; }; export type ErrorHandler<ErrorShape> = (ctx: ErrorContext<ErrorShape>) => void; export type InputHandler = (event: FormInputEvent) => void; export type BlurHandler = (event: FormFocusEvent) => void; export type SubmitContext<FormShape extends Record<string, any> = Record<string, any>, ErrorShape = any, Value = undefined> = { formData: FormData; value: Value; update: (options: { error?: Partial<FormError<ErrorShape>> | null | undefined; value?: FormShape | null | undefined; reset?: boolean | undefined; }) => void; }; export type SubmitHandler<FormShape extends Record<string, any> = Record<string, any>, ErrorShape = any, Value = undefined> = (event: React.FormEvent<HTMLFormElement>, ctx: SubmitContext<FormShape, ErrorShape, Value>) => void | Promise<void>; /** * Infer the base error shape from a FormsConfig. * * **Example:** * ```ts * const { config } = configureForms({ isError: shape<{ message: string }>() }); * type ErrorShape = InferBaseErrorShape<typeof config>; // { message: string } * ``` */ export type InferBaseErrorShape<Config> = Config extends FormsConfig<infer ErrorShape, any, any, any, any> ? ErrorShape : string; /** * Infer the custom form metadata extension from a FormsConfig. * Use this to compose with FormMetadata, FieldMetadata, or Fieldset types. * * **Example:** * ```ts * const { config } = configureForms({ * extendFormMetadata: (meta) => ({ customProp: meta.id }) * }); * type MyFormMetadata = FormMetadata< * InferBaseErrorShape<typeof config>, * InferCustomFormMetadata<typeof config>, * InferCustomFieldMetadata<typeof config> * >; * ``` */ export type InferCustomFormMetadata<Config> = Config extends FormsConfig<any, any, any, infer CustomFormMetadata, any> ? CustomFormMetadata : {}; /** * Infer the custom field metadata extension from a FormsConfig. * Use this to compose with FieldMetadata or Fieldset types. * * **Example:** * ```ts * const { config } = configureForms({ * extendFieldMetadata: (meta) => ({ inputProps: { name: meta.name } }) * }); * type MyFieldMetadata<T> = FieldMetadata<T, InferBaseErrorShape<typeof config>, InferCustomFieldMetadata<typeof config>>; * type MyFieldset<T> = Fieldset<T, InferBaseErrorShape<typeof config>, InferCustomFieldMetadata<typeof config>>; * ``` */ export type InferCustomFieldMetadata<Config> = Config extends FormsConfig<any, any, any, any, infer CustomFieldMetadata> ? CustomFieldMetadata : {}; /** * Transform a type to make specific keys conditional based on FieldShape. * Keys in ConditionalKeys will only be present when FieldShape extends the specified type. * Uses ConditionalFieldMetadata wrapper that RestoreFieldShape will detect and evaluate. * * **Example:** * ```ts * type Result = MakeConditional< * { textFieldProps: {...}, dateRangePickerProps: {...} }, * { dateRangePickerProps: { start: string; end: string } } * >; * // dateRangePickerProps is wrapped with ConditionalFieldMetadata * // and will only be present when FieldShape extends { start: string; end: string } * ``` */ export type MakeConditional<T, ConditionalKeys extends Record<string, unknown>> = Omit<T, keyof ConditionalKeys> & { [K in keyof ConditionalKeys]: K extends keyof T ? ConditionalFieldMetadata<T[K], ConditionalKeys[K]> : never; }; export type MaybePromise<T> = T | Promise<T>; type BaseFieldsetProps = RequireKey<Omit<React.ComponentPropsWithoutRef<'fieldset'>, 'children' | 'defaultValue'>, 'name'> & { /** * Renders a hidden `<fieldset>` base control with nested hidden `<input>` elements * derived from `defaultValue`. */ type: 'fieldset'; /** * Structured default value used to render nested hidden inputs. */ defaultValue: unknown; }; type BaseSelectProps = RequireKey<Omit<React.ComponentPropsWithoutRef<'select'>, 'children' | 'value'>, 'name' | 'defaultValue'> & { /** * Renders a hidden `<select>` base control. */ type: 'select'; }; type BaseTextareaProps = RequireKey<Omit<React.ComponentPropsWithoutRef<'textarea'>, 'children' | 'value'>, 'name' | 'defaultValue'> & { /** * Renders a hidden `<textarea>` base control. */ type: 'textarea'; }; type BaseCheckedInputProps = RequireKey<Omit<React.ComponentPropsWithoutRef<'input'>, 'children' | 'type' | 'checked'>, 'name' | 'defaultChecked'> & { /** * Renders a hidden checkbox or radio base control. */ type: 'checkbox' | 'radio'; }; type BaseFileInputProps = RequireKey<Omit<React.ComponentPropsWithoutRef<'input'>, 'children' | 'type' | 'value' | 'checked'>, 'name'> & { /** * Renders a hidden `<input type="file">` base control. */ type: 'file'; }; type BaseInputProps = RequireKey<Omit<React.ComponentPropsWithoutRef<'input'>, 'children' | 'type' | 'value' | 'checked'>, 'name' | 'defaultValue'> & { /** * Renders a hidden `<input type="...">` base control. */ type?: 'color' | 'date' | 'datetime-local' | 'email' | 'hidden' | 'month' | 'number' | 'password' | 'range' | 'search' | 'tel' | 'text' | 'time' | 'url' | 'week'; }; export type BaseControlProps = BaseFieldsetProps | BaseSelectProps | BaseTextareaProps | BaseCheckedInputProps | BaseFileInputProps | BaseInputProps; export {}; //# sourceMappingURL=types.d.ts.map