import { StandardSchemaV1 } from "../standard-schema/types.mjs"; import { FillOptions, FormControl, FormControlValues, FormEventType, FormInterface, ValidateOn } from "../types.mjs"; import { EventSubscription } from "@mongez/events"; import React from "react"; //#region ../react-form/src/engine/FormEngine.d.ts /** * Mutable options synced into the engine each render by the host component. */ type FormEngineOptions = { id?: string; defaultValue?: Record; values?: Record; schema?: StandardSchemaV1; validateOn?: ValidateOn; ignoreEmptyValues?: boolean; focusFirstError?: boolean; onSubmit?: (options: { form: FormInterface; event?: React.FormEvent; values: any; formData: FormData; }) => void | Promise; onError?: (invalidControls: FormControl[]) => void; }; /** * Platform-agnostic, **React-free** form engine — the controller object behind * every `
` / ``. * * It is a plain class (not a `React.Component`) so it can be unit-tested in * isolation and held in a `useRef` by the host function component. The host * owns rendering and the host element; the engine owns *everything else* — * registration, validation, value collection, dirty tracking, hydration, and * the submit pipeline. * * ## Mental model * * The engine never re-renders anything itself. It coordinates the form through * three cooperating mechanisms — keep these in mind when reading any method: * * 1. **Mutable instance state** (`formControls`, `invalidControls`, * `dirtyControls`, `_isSubmitting`, …). Plain arrays/flags mutated in place. * Mutating them is intentionally invisible to React; that is what keeps the * library fast (registering a control or flipping validity does not re-render * the tree). * 2. **A pub/sub event bus** (`@mongez/events`). Every event is namespaced under * `form.{id}.{event}` (see {@link on} / {@link trigger}). Consumers like * `useSubmitButton` / `useWatch` and the host subscribe via {@link on}; the * engine emits via {@link trigger}. Per-control value changes flow on a * separate key, `form.control.{controlId}.change`, owned by the control. * 3. **Control registration.** Each `useFormControl` builds a mutable * `FormControl` object and calls {@link register} on mount / {@link unregister} * on unmount. The engine reads/writes those objects directly (e.g. * `control.value`, `control.validate()`); it never copies their state. * * ## Lifecycle (driven by the host component) * * ```text * new FormEngine(options) // construct once, seed id + defaultValue + values + schema * → activate() // mount effect: register as the active form * → setOptions(options) // every render: re-sync scalar handlers (onSubmit/onError/…) * → fill() / setDefaultValue() // when the reactive `values` / `defaultValue` prop changes * → register() / unregister() // as controls mount / unmount * → handleSubmit() / validate() // on submit (native event or form.submit()) * → destroy() // unmount effect: drop active-form registration * ``` * * The host injects {@link submitHandler} so `form.submit()` can trigger a native * DOM submit on web vs. run the pipeline directly on React Native. */ declare class FormEngine implements FormInterface { /** * Reference to the host element (HTMLFormElement on web, any on native). * Assigned by the host component via its ref callback. */ formElement: any; /** * Form id. */ protected formId: string; /** * Form event prefix. */ protected formEventPrefix: string; /** * Form controls. */ protected formControls: FormControl[]; /** * Determine whether form validation is valid. */ protected isValidForm: boolean; /** * Determine form submission state. */ protected _isSubmitting: boolean; /** * Determine if form is disabled. */ protected _isDisabled: boolean; /** * List of invalid controls. */ protected invalidControls: FormControl[]; /** * List of valid controls. */ protected validControls: FormControl[]; /** * Form-level error messages with no owning control — produced by whole-form * schema issues whose path maps to no control (cross-field / root errors). */ formErrors: React.ReactNode[]; /** * Dirty controls. */ dirtyControls: FormControl[]; /** * Default value (reset baseline). */ defaultValue: Record | undefined; /** * Reactive hydration snapshot (nested object) — read by controls mounting * after a `fill()` / reactive `values` update. */ hydrationValues: Record | undefined; /** * Whole-form Standard Schema validator. */ schema: StandardSchemaV1 | undefined; /** * Form-level default validation trigger. */ validateOn: ValidateOn | undefined; /** * Whether the form has attempted submission at least once. */ wasSubmitted: boolean; /** * Form control change subscriptions, keyed by control id/name. */ protected formControlEvents: Record; /** * Current form dirty state. */ isDirty: boolean; /** * Latest mutable options synced from the host component. */ protected formOptions: FormEngineOptions; /** * Seeds the one-time state: the (stable) form id + its event prefix, the * reset baseline (`defaultValue`), the initial hydration snapshot (cloned from * `values` so later `fill()` merges don't mutate the caller's object), the * whole-form schema, and the default validation trigger. * * Everything that can change between renders (the `onSubmit` / `onError` * handlers, `ignoreEmptyValues`, …) is re-read from {@link setOptions}, not * captured here. */ constructor(options?: FormEngineOptions); /** * Sync the latest scalar options/handlers from the host component. Reactive * `values` / `defaultValue` identity changes are handled by the host calling * `fill()` / `setDefaultValue()` explicitly. */ setOptions(options: FormEngineOptions): void; /** * Register the engine as the active form. Called by the host on mount. */ activate(): void; /** * Tear down active-form registration. Called by the host on unmount. */ destroy(): void; change(name: string, value: any): void; /** Move a control into the invalid bucket and mark the form invalid. */ invalidControl(formControl: FormControl): void; /** * Move a control into the valid bucket; the form is valid again only once the * invalid bucket is empty. */ validControl(formControl: FormControl): void; /** * Debounced aggregate validity check. Call it after a batch of per-control * validity changes; it emits a single `validControls` / `invalidControls` * event on the next tick. (A field initializer rather than a method so each * engine instance owns its own debounced function.) */ checkIfIsValid: ((this: unknown) => void) & { cancel(): void; flush(): void; pending(): boolean; }; protected _checkIfIsValid(): ((this: unknown) => void) & { cancel(): void; flush(): void; pending(): boolean; }; /** * Set the in-flight submit state and emit `submitting`. Clearing it * (`submitting(false)`) *also* emits `submit` — that is the single completion * signal for an async submit (see {@link handleSubmit}). Call this yourself in * a sync `onSubmit`'s success/failure path to re-enable a submit button. */ submitting(submitting: boolean): void; disable(isDisabled?: boolean): this; enable(): this; /** Whether the whole form is currently disabled (via {@link disable}). */ isDisabled(): boolean; isSubmitting(): boolean; isValid(): boolean; get id(): string; /** * Subscribe to a form event. Returns an `EventSubscription` — call * `.unsubscribe()` (typically in a `useEffect` cleanup) to detach. */ on(event: FormEventType, callback: (form: FormInterface) => void): EventSubscription; /** Emit a form event (fire-and-forget; listener return values are ignored). */ trigger(event: FormEventType, ...values: any[]): any; /** * Emit a form event and collect every listener's return value in * `response.results`. Used by {@link validate} for the `validating` veto: a * listener returning `false` aborts validation. */ triggerAll(event: FormEventType, ...values: any[]): import("@mongez/events").EventTriggerResponse; /** * Validate `controls` (the whole form by default) and recompute form validity. * * Flow: * 1. Reset the valid/invalid buckets and optimistically assume valid. * 2. Fire `validating` — any listener returning `false` **vetoes** the run * (the form is marked invalid and nothing else validates). * 3. `await` each control's `validate()` in turn. This is what makes async * rules genuinely gate submission: a control whose rule returns a Promise * is awaited here before the form decides it is valid. Each control sorts * itself into the invalid/valid bucket via its resolved `isValid`. * 4. If a whole-form {@link schema} is set, run it and map issues back to * controls (see {@link validateSchema}). * 5. Emit `validation`, schedule the debounced aggregate event, and call the * `onError` handler when invalid. * * `validateVisible()` passes a filtered `controls` subset; schema issues for * controls outside that subset are ignored so hidden fields don't fail it. */ validate(controls?: FormControl[] | string[]): Promise; /** * Run the whole-form Standard Schema and distribute its issues. Three cases: * * - **Maps to a control in the validated subset** → shown on that control. * - **Maps to a control OUTSIDE the subset** (e.g. a hidden wizard step during * `validateVisible()`) → ignored, so a subset validation isn't failed by * fields it deliberately skipped. * - **Root / cross-field issue (empty path)** → there is no control to own it, * so it becomes a {@link formErrors} entry and blocks submission. (These * were previously dropped, letting the form submit schema-invalid.) * * A non-empty path matching no control is treated as a schema/form-name * mismatch and ignored, so a stray path can't make the form permanently * unsubmittable. */ protected validateSchema(controls: FormControl[]): Promise; /** * Force a control into an invalid state with the given message. */ protected applyControlError(control: FormControl, message: React.ReactNode): void; validateVisible(): Promise; /** * Add a control to the form and wire its change subscription. * * - The leading guard makes registration idempotent (a control re-running its * effect, e.g. a field-array row whose name/index changed, won't be added * twice). * - The `onChange` subscription is the engine's link to per-control changes: * it keeps `dirtyControls` (and thus `isDirty`) in sync and re-broadcasts a * form-level `change` event for `useWatch` / `useFieldArray`. * - The subscription is stored under the control's key so {@link unregister} * can tear it down. */ register(formControl: FormControl): void; /** * Remove a control and undo everything {@link register} set up: notify the * control (`unregister`), drop it from `formControls` and the invalid bucket, * unsubscribe its change listener, and recompute dirty/validity so the form * doesn't stay blocked by a control that no longer exists. */ unregister(formControl: FormControl): void; protected setIsDirty(isDirty: boolean): void; control(value: string, getBy?: "name" | "id"): FormControl | null; /** * Restore the form to its pristine state: each control resets to its * `initialValue` (the reset baseline — see {@link getResetBaseline}) and * clears its error/dirty/touched flags, then the form-level flags reset. * Brackets the work with `resetting` (before) and `reset` (after) events. * * Pass `values` to reset to a NEW baseline (e.g. after saving an edit form): * the new values are merged into `defaultValue` and become each control's * reset target, so subsequent resets restore to them too. */ reset(values?: Record): this; resetErrors(): this; /** * Resolve the seed (display) value for a control name: the reactive hydration * snapshot wins over the reset baseline. */ getInitialValue(name: string): any; /** * Resolve the **reset baseline** for a control name — from `defaultValue` * only, never the hydration snapshot. `form.reset()` restores controls to * this, so live-loaded `values` never become the reset target. */ getResetBaseline(name: string): any; /** * Bulk-write values onto mounted controls and seed later-mounting controls. * Does not change the reset baseline (`defaultValue`). */ fill(values: Record, options?: FillOptions): this; setValues(values: Record, options?: FillOptions): this; /** * Update the reset baseline. Pristine (non-dirty) controls re-hydrate to the * new baseline; dirty controls keep the user's edits. */ setDefaultValue(defaultValue: Record | undefined): this; setErrors(errors: Record): this; /** Current value of a single control by name (live, not collected). */ value(formControlName: string): any; /** * The collected form values as a nested object: takes the flat dot-notation * map from {@link collectValues} and expands it (`user.address.city` → * `{ user: { address: { city } } }`, numeric segments → arrays). This is what * `onSubmit` receives as `values`. */ values(formControlNames?: string[]): any; shouldIgnoreEmptyValues(): boolean; /** * Collect a FLAT `{ dotNotationName: value }` map from the registered * controls — the raw material {@link values} nests into objects/arrays. * * Per control: skip the unnamed and the non-collectable (disabled, unchecked * boxes, …); optionally drop empties when `ignoreEmptyValues` is on; then * merge into the map. The merge collapses repeats into an array — if a name * already has a value (or the control is `multiple`), the slot is promoted to * an array and subsequent values are pushed. That is how N inputs sharing one * `name` (e.g. a checkbox group) become a single array value. */ collectValues(formControlNames?: string[]): FormControlValues; /** * The collected values as a `FormData` for `multipart/form-data` submits. * Mirrors the nested shape using PHP/Rails-style bracket keys on the wire: * arrays as `name[]`, plain objects as `name[key]`. */ formData(): FormData; controls(formControls?: string[]): FormControl[]; /** * Manually submit the form. Delegates to the host-injected * {@link submitHandler} because only the host knows how to dispatch a native * submit on web vs. run the pipeline directly on React Native. */ submit(): void; /** * Submit trigger injected by the host component (see {@link submit}). */ submitHandler: (() => void) | undefined; /** * The shared submit pipeline. * * Flow: * 1. Mark `wasSubmitted` (drives `validateOn: "submit"` revalidation). * 2. `await validate()` — async rules block here. * 3. Bail if invalid, or if a submit is already in flight (re-entrancy guard). * 4. With no `onSubmit`, emit `submit` immediately and stop. * 5. Otherwise flip submitting on and invoke `onSubmit` with lazy `values` / * `formData` getters (re-collected on access). * - **Async** `onSubmit`: defer clearing the submitting state AND the * `submit` completion event until the returned promise settles, so * `submit` fires exactly once, after the work is done. (`submitting(false)` * is itself what emits `submit` here — see {@link submitting}.) * - **Sync / void** `onSubmit`: completion is immediate; the caller owns * clearing the submitting state. * - A synchronous throw clears the submitting state and re-throws. */ handleSubmit(event?: React.FormEvent): Promise; } declare function createNestedObjectFromDotNotation(object: any): any; //#endregion export { FormEngine, FormEngineOptions, createNestedObjectFromDotNotation }; //# sourceMappingURL=FormEngine.d.mts.map