UNPKG

pocket-hook-form

Version:

pocket-store base hook form

268 lines (233 loc) 7.9 kB
import {RefObject} from 'react'; import type {Listener} from 'pocket-state'; interface FileList { readonly length: number; item(index: number): File | null; [index: number]: File; } export type CustomElement<T extends FormValue> = Partial<HTMLElement> & { name: FieldName<T>; type?: string; value?: any; disabled?: boolean; checked?: boolean; options?: HTMLOptionsCollection; files?: FileList | null; focus?: Noop; text?: string; }; export type FieldElement<T extends FormValue = FormValue> = | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | CustomElement<FormValue>; /**Primitive type */ export type Primitive = | string | number | bigint | boolean | symbol | null | undefined; export type ErrorType = | 'required' | 'minLength' | 'maxLength' | 'pattern' | 'validate' | (string & {}); /** * Any object used as the form's value bag. * Keys are field names, values are field values. */ export type FormValue = Record<string, any>; /** input handler */ export interface InternalFieldRefHandle { focus?: () => void; blur?: () => void; clear?: () => void; open?: () => void; } /** * Minimal interface for focusable inputs. * You can attach any extra properties your input or any component exposes. */ export type FieldRef< T extends InternalFieldRefHandle = InternalFieldRefHandle, > = T | null; /** * Validation rules for a single field value. * * - `required`: boolean or a message string (truthy enables required) * - `minLength` / `maxLength`: number or { value, message } * - `pattern`: RegExp or { value, message } * - `validate`: custom validator; return: * - `true` or `undefined` → passes * - `string` → error message */ export type FieldRules<V, T extends FormValue> = { required?: boolean | string; minLength?: {value: number; message?: string} | number; maxLength?: {value: number; message?: string} | number; pattern?: {value: RegExp; message?: string} | RegExp; validate?: (value: V, allValues: T) => string | true | undefined; }; /** * A single validation error for a field. * - `type`: the rule that failed (e.g., "required", "minLength", "pattern", "validate") * - `message`: optional human-readable message */ export type FieldError = { type: ErrorType; message?: string; }; /** * Error bag keyed by field name. * Each field can hold one error or a list of errors (when `criteriaMode: 'all'`). */ export type FormErrors<T extends FormValue> = Partial<{ [K in keyof T]: FieldError | FieldError[]; }>; /** How multiple errors per field are collected. */ export type CriteriaMode = 'firstError' | 'all'; /** When validation runs automatically. */ export type Mode = 'onSubmit' | 'onChange' | 'onBlur'; /** * Options for creating a form control. * * - `defaultValues`: initial values for all fields * - `mode`: when to auto-validate (default: 'onSubmit') * - `criteriaMode`: collect only first error or all (default: 'firstError') * - `rules`: per-field validation rules */ export type CreateOptions<T extends FormValue> = { defaultValues: T; mode?: Mode; criteriaMode?: CriteriaMode; rules?: Partial<{[K in keyof T]: FieldRules<T[K], T>}>; }; /** * The full form state kept in the store. * - `values`: current values * - `errors`: current validation errors * - `touched`: fields that have been blurred/interacted with * - `dirty`: fields changed from their defaults * - `submitting`: true while `handleSubmit` is running */ export type FormState<T extends FormValue> = { values: T; errors?: FormErrors<T>; touched: Partial<Record<keyof T, boolean>>; dirty: Partial<Record<keyof T, boolean>>; submitting: boolean; }; /** * Public API exposed by the form controller + its underlying store. * Pair with hooks like `useFormState` and components like `Controller`. */ export type FormStoreControl<T extends FormValue, TRef = null> = { /** * Register/unregister a field with an optional component ref. */ addRef: (name: keyof T, ref: FieldRef) => void; removeRef: (name: keyof T) => void; /** Get a previously registered ref (or null). */ getRef: <TRef extends InternalFieldRefHandle = InternalFieldRefHandle>( name: keyof T, ) => FieldRef<TRef>; /** * Update a field's value. * - `validate`: revalidate the field after setting * - `markDirty`: mark the field as dirty */ setValue: <K extends keyof T>( name: K, value: T[K], opts?: {validate?: boolean; markDirty?: boolean}, ) => void; /** Read a single field value. */ getField: <K extends keyof T>(name: K) => T[K]; /** Read the entire values object. */ getValues: () => T; /** Set a single field error (overwrites existing). */ setError: <K extends keyof T>(name: K, error: FieldError) => void; /** Replace the entire error bag. */ setErrors: (errors: FormErrors<T>) => void; /** Clear all errors. */ resetErrors: () => void; /** Clear errors for specific fields. */ clearErrors: <K extends keyof T>(...names: K[]) => void; /** Focus a registered field if it has a focusable ref. */ focus: (name: keyof T) => void; setTouched: <K extends keyof T>(name: K, touched?: boolean) => void; resetTouched: () => voi; /** * Wrap a submit handler with validation + submitting lifecycle. * Returns an async function suitable for onPress/onSubmitEditing. */ handleSubmit: ( fn: (values: T) => void | Promise<void>, ) => (e?: any) => Promise<void>; /** * Reset values to defaults or to a provided partial. * Also clears `dirty`, `touched`, and `errors`. */ reset: (next?: Partial<T>) => void; getStoreOptions: () => CreateOptions<T>; updateOptions: (next: Partial<CreateOptions<T>>) => void; /** * Subscribe to the entire state. * The listener is invoked **after commit** whenever state changes. * @returns Unsubscribe function. */ subscribe(listener: Listener<FormState<T>>): () => void; /** * Subscribe to a derived slice of the state. * The listener fires only when the selector's result changes. * @returns Unsubscribe function. */ subscribe<S>( selector: (state: FormState<T>) => S, listener: Listener<S>, ): () => void; /** * Underlying pocket-state store carrying the FormState<T>. * Use with `useStore`/`useFormState` to subscribe efficiently. */ // #private; // readonly _store: import('pocket-state').Store<FormState<T>>; }; export type FieldState = { /** Has the user interacted with (blurred) this field? */ touched: boolean; /** Has the value diverged from the initial/default value? */ isDirty: boolean; /** Current validation error(s) associated with this field. */ errors?: FieldError[]; }; /** * Return shape of `useController<T, K>` for a single field. * * - `value`: current field value * - `onChange(v)`: update value (usually wired to `onChangeText`) * - `onBlur()`: mark touched & trigger validation on blur * - `ref`: input ref (e.g., React Native `TextInput`) used for focus control * - `errors`: same as `fieldState.error` (kept for backward compatibility) * - `onSubmitEditing()`: move focus to next field or submit (keyboard "Next"/"Done") * - `fieldState`: meta info (touched/dirty/error) for easy UI rendering */ export type UseControllerReturn<T extends FormValue, K extends keyof T> = { /** Current field value. */ value: T[K]; /** Update the field value and (optionally) validate/mark dirty upstream. */ onChange: (...e: any) => void; /** Trigger validation and mark the field as touched. */ onBlur: () => void; /** Ref to the underlying input component for focus management. */ ref: React.RefObject<any>; /** Handler for keyboard submit/next; typically focuses the next field. */ onSubmitEditing: () => void; /** Meta state for this field (touched/dirty/error). */ fieldState: FieldState; };