UNPKG

@mobx-sentinel/form

Version:

MobX library for non-intrusive class-based model enhancement. Acting as a sentinel, it provides change detection, reactive validation, and form integration capabilities without contamination.

499 lines (492 loc) 17.8 kB
import { Validator, Watcher, KeyPath } from '@mobx-sentinel/core'; import * as mobx from 'mobx'; declare const internalToken$1: unique symbol; /** * Form field that tracks input state and validation errors * * Key features: * - Tracks touch state * - Handles intermediate (partial) input states * - Manages error reporting * - Supports auto-finalization of intermediate values */ declare class FormField { #private; readonly id: string; readonly fieldName: string; readonly validator: Validator<any>; /** @ignore */ constructor(args: { fieldName: string; validator: Validator<any>; getFinalizationDelayMs: () => number; }); /** * Whether the field is touched. * * A field becomes touched when the user interacts with it. */ get isTouched(): boolean; /** * Whether the field value is intermediate (partial input) * * @example * - Typing "user@" in an email field * - Typing a partial date "2024-" * * @remarks * Intermediate values are automatically finalized after a delay */ get isIntermediate(): boolean; /** * Whether the field value is changed */ get isChanged(): boolean; /** * Whether the error states has been reported. * * Check this value to determine if errors should be displayed to the user. * * @returns * - `undefined` - Validity is undetermined (not yet reported) * - `false` - Field is valid * - `true` - Field is invalid * * @remarks * - Error reporting is delayed until validation is complete. * - It can be used directly with the `aria-invalid` attribute. */ get isErrorReported(): boolean | undefined; /** * Error messages for the field * * Regardless of {@link isErrorReported}, this value is always up-to-date. */ get errors(): ReadonlySet<string>; /** * Whether the field has errors * * Regardless of {@link isErrorReported}, this value is always up-to-date. */ get hasErrors(): boolean; /** * Reset the field state * * @remarks * - Clears touched state * - Clears change state * - Clears error reporting * - Cancels any pending auto-finalization */ reset(): void; /** * Mark the field as touched * * It's usually triggered by `onFocus`. */ markAsTouched(): void; /** * Mark the field as changed * * It's usually triggered by `onChange`. * * @param type Default to "final" * * @remarks * - "final" immediately reports errors * - "intermediate" schedules auto-finalization after delay */ markAsChanged(type?: FormField.ChangeType): void; /** * Report the errors of the field. * * It will wait until the validation is up-to-date before reporting the errors. */ reportError(): void; /** Finalize the intermediate change if needed (usually triggered by onBlur) */ finalizeChangeIfNeeded(): void; /** @internal @ignore */ [internalToken$1](): { isReported: mobx.IObservableValue<boolean>; }; } declare namespace FormField { /** Strict field name */ type NameStrict<T> = keyof T & string; /** Augmented field name with an arbitrary suffix */ type NameAugmented<T> = `${NameStrict<T>}:${string}`; /** Field name */ type Name<T> = NameStrict<T> | NameAugmented<T>; /** * The type of change that has occurred in the field. * * - "final" - The input is complete and no further update is necessary to make it valid. * - "intermediate" - The input is incomplete and does not yet conform to the expected format. */ type ChangeType = "final" | "intermediate"; } type ConfigOf<T> = T extends new (form: Form<any>, config: infer Config) => FormBinding ? Config : T extends new (field: FormField, config: infer Config) => FormBinding ? Config : T extends new (fields: FormField[], config: infer Config) => FormBinding ? Config : never; /** Interface for form binding classes */ interface FormBinding { /** Configuration of the binding */ config?: object; /** Binding properties which passed to the view component */ readonly props: object; } /** Polymorphic function of Form#bind */ interface FormBindingFunc<T> extends FormBindingFunc.ForField<T>, FormBindingFunc.ForMultiField<T>, FormBindingFunc.ForForm<T> { } declare namespace FormBindingFunc { /** Bind configuration */ type Config = { /** Cache key for the binding */ cacheKey?: string; }; /** Bind to a field */ interface ForField<T> { /** Create a binding for the field */ <Binding extends new (field: FormField) => FormBinding>(fieldName: FormField.Name<T>, binding: Binding, config?: Config): InstanceType<Binding>["props"]; /** Create a binding for the field with the config */ <Binding extends new (field: FormField, config: any) => FormBinding>(fieldName: FormField.Name<T>, binding: Binding, config: NoInfer<ConfigOf<Binding>> & Config): InstanceType<Binding>["props"]; } /** Bind to multiple fields */ interface ForMultiField<T> { /** Create a binding for the multiple fields */ <Binding extends new (fields: FormField[]) => FormBinding>(fieldNames: FormField.Name<T>[], binding: Binding, config?: Config): InstanceType<Binding>["props"]; /** Create a binding for the multiple fields with the config */ <Binding extends new (fields: FormField[], config: any) => FormBinding>(fieldNames: FormField.Name<T>[], binding: Binding, config: NoInfer<ConfigOf<Binding>> & Config): InstanceType<Binding>["props"]; } /** Bind to the form */ interface ForForm<T> { /** Create a binding for the form */ <Binding extends new (form: Form<T>) => FormBinding>(binding: Binding, config?: Config): InstanceType<Binding>["props"]; /** Create a binding for the form with the config */ <Binding extends new (form: Form<T>, config: any) => FormBinding>(binding: Binding, config: NoInfer<ConfigOf<Binding>> & Config): InstanceType<Binding>["props"]; } } declare namespace FormBindingFuncExtension { /** Bind configuration */ type Config = FormBindingFunc.Config; /** Bind to a field */ namespace ForField { /** Create a binding for the field with an optional config */ type OptionalConfig<T, Binding extends new (field: FormField, config?: any) => FormBinding> = (fieldName: FormField.Name<T>, config?: NoInfer<ConfigOf<Binding>> & Config) => InstanceType<Binding>["props"]; /** Create a binding for the field with a required config */ type RequiredConfig<T, Binding extends new (field: FormField, config: any) => FormBinding> = (fieldName: FormField.Name<T>, config: NoInfer<ConfigOf<Binding>> & Config) => InstanceType<Binding>["props"]; } /** Bind to multiple fields */ namespace ForMultiField { /** Create a binding for the multiple fields */ type OptionalConfig<T, Binding extends new (fields: FormField[], config?: any) => FormBinding> = (fieldNames: FormField.Name<T>[], config?: NoInfer<ConfigOf<Binding>> & Config) => InstanceType<Binding>["props"]; /** Create a binding for the multiple fields with the config */ type RequiredConfig<T, Binding extends new (fields: FormField[], config: any) => FormBinding> = (fieldNames: FormField.Name<T>[], config: NoInfer<ConfigOf<Binding>> & Config) => InstanceType<Binding>["props"]; } /** Bind to the form */ namespace ForForm { /** Create a binding for the form */ type OptionalConfig<T, Binding extends new (form: Form<T>, config: any) => FormBinding> = (config?: NoInfer<ConfigOf<Binding>> & Config) => InstanceType<Binding>["props"]; /** Create a binding for the form with the config */ type RequiredConfig<T, Binding extends new (form: Form<T>, config: any) => FormBinding> = (config: NoInfer<ConfigOf<Binding>> & Config) => InstanceType<Binding>["props"]; } } /** Form configuration */ type FormConfig = { /** * Automatically finalize the form when the input is intermediate (partial input). [in milliseconds] * * @default 3000 */ autoFinalizationDelayMs: number; /** * Allow submission even if the form is not dirty. * * @default false */ allowSubmitNonDirty: boolean; /** * Allow submission even if the form is invalid. * * @default false */ allowSubmitInvalid: boolean; }; /** Update the global configuration */ declare function configureForm(config: Partial<Readonly<FormConfig>>): Readonly<FormConfig>; /** Reset the global configuration to the default */ declare function configureForm(reset: true): Readonly<FormConfig>; /** * Manages form submission lifecycle and handlers * * Key features: * - Handles submission lifecycle (`willSubmit` -> `submit` -> `didSubmit`) * - Executes handlers in registration order * - Supports cancellation of in-progress submissions * - Tracks submission state */ declare class Submission { #private; /** Whether the submission is running */ get isRunning(): boolean; /** * Add a handler for submission events * * @returns Function to remove the handler */ addHandler<K extends keyof Submission.Handlers>(event: K, handler: Submission.Handlers[K]): () => void; /** * Execute the submission process * * @remarks * - Cancels any in-progress submission * - Executes handlers in order * - `submit` handlers are executed serially * - Aborts if any `submit` handler returns false * - Handles exceptions in all phases * * @returns `true` if submission succeeded, `false` if failed or aborted */ exec(): Promise<boolean>; } declare namespace Submission { /** @inline */ type Handlers = { /** * Called before submission starts * @param abortSignal - Abort signal for the submission * @returns `true` to continue submission, `false` to cancel submission */ willSubmit: (abortSignal: AbortSignal) => Promise<boolean>; /** * Async handler that perform the submission (serialized) * * @param abortSignal - Abort signal for the submission * @returns `true` if submission succeeded, `false` if failed or aborted */ submit: (abortSignal: AbortSignal) => Promise<boolean>; /** * Called after submission completes * * @param succeed - `true` if submission succeeded, `false` if failed or aborted */ didSubmit: (succeed: boolean) => void; }; } declare const internalToken: unique symbol; /** * Form manages submission, fields, and bindings. * * Key features: * - Manages form fields and their states * - Supports nested forms * - Manages form submission lifecycle * - Provides binding system for UI integration * - Integrates with Watcher for change detection * - Integrates with Validator for validation */ declare class Form<T> { #private; readonly id: string; readonly watcher: Watcher; readonly validator: Validator<T>; /** Extension fields for bindings */ [k: `bind${Capitalize<string>}`]: unknown; /** * Get a form instance for the subject. * * @remarks * - Returns existing instance if one exists for the subject * - Creates new instance if none exists * - Instances are cached and garbage collected with their subjects * - Multiple forms per subject supported via `formKey` * * @param subject The model object to create form for * @param formKey Optional key for multiple forms per subject * * @throws `TypeError` if subject is not an object */ static get<T extends object>(subject: T, formKey?: symbol): Form<T>; /** * Get a form instance for the subject. * * Same as {@link Form.get} but returns null instead of throwing an error. */ static getSafe<T extends object>(subject: T, formKey?: symbol): Form<T> | null; /** * Manually dispose the form instance for the subject. * * Use with caution.\ * You don't usually need to use this method at all.\ * It's only for advanced use cases, such as testing. * * @see {@link Form.get} */ static dispose(subject: object, formKey?: symbol): void; private constructor(); /** * The configuration of the form * * This is a computed value that combines the global configuration and the local configuration. */ get config(): Readonly<FormConfig>; /** Configure the form locally */ configure: { /** Override the global configuration locally */ (config: Partial<Readonly<FormConfig>>): void; /** Reset to the global configuration */ (reset: true): void; }; /** * Whether the form is dirty (including sub-forms) * * @remarks Alias for {@link Watcher.changed}. */ get isDirty(): boolean; /** * Whether the form is valid (including sub-forms) * * @remarks Alias for {@link Validator.isValid}. */ get isValid(): boolean; /** * The number of invalid fields * * @remarks Alias for {@link Validator.invalidKeyCount}. */ get invalidFieldCount(): number; /** * The number of total invalid field paths (counts invalid fields in sub-forms) * * @remarks Alias for {@link Validator.invalidKeyPathCount}. */ get invalidFieldPathCount(): number; /** * Whether the form is in validator state * * @remarks Alias for {@link Validator.isValidating}. */ get isValidating(): boolean; /** Whether the form is in submitting state */ get isSubmitting(): boolean; /** Whether the form is busy (submitting or validating) */ get isBusy(): boolean; /** * Whether the form can be submitted * * @remarks * - Checks if the form is not busy * - Checks if the form is valid or allows invalid submissions * - Checks if the form is dirty or allows non-dirty submissions */ get canSubmit(): boolean; /** * Sub-forms within the form. * * Forms are collected via `@nested` annotation. */ get subForms(): ReadonlyMap<KeyPath, Form<any>>; /** Report error states on all fields and sub-forms */ reportError(): void; /** * Reset the form's state * * @remarks * - Resets the fields * - Resets the sub-forms * - Resets the watcher * - Does not reset the validator */ reset(): void; /** * Mark the form as dirty * * @remarks Alias for {@link Watcher.assumeChanged}. */ markAsDirty(): void; /** * Submit the form. * * @remarks * - Checks if {@link canSubmit} is `true` * - Executes handlers in order * - Aborts if any `submit` handler returns `false` * - Handles exceptions in all phases * - Resets the form after successful submission * * @param args.force Whether to force submission even if {@link canSubmit} is `false`. * It cancels any in-progress submission if any. * * @returns `true` if submission succeeded, `false` if failed or aborted */ submit(args?: { force?: boolean; }): Promise<boolean>; /** * Add a submission handler * * @remarks * Handlers are called in this order: * 1. `willSubmit` - Called before submission starts * 2. `submit` - Async handlers that perform the submission (serialized) * 3. `didSubmit` - Called after submission completes * * @see {@link Form.Handlers} * * @returns Function to remove the handler */ addHandler: { (event: "willSubmit", handler: Submission.Handlers["willSubmit"]): () => void; (event: "submit", handler: Submission.Handlers["submit"]): () => void; (event: "didSubmit", handler: Submission.Handlers["didSubmit"]): () => void; }; /** * Get a field by name * * @remarks Fields are cached and reused. */ getField(fieldName: FormField.Name<T>): FormField; /** * Create UI binding for form elements * * @remarks * - Can bind to individual fields * - Can bind to multiple fields * - Can bind to the entire form * - Bindings are cached and reused * - Supports configuration via binding classes */ bind: FormBindingFunc<T>; /** * Get error messages for a field * * @param fieldName Field to get errors for * @param includePreReported Whether to include errors not yet reported * * @returns Set of error messages */ getErrors(fieldName: FormField.Name<T>, includePreReported?: boolean): ReadonlySet<string>; /** * Get all error messages for the form * * @param fieldName Field to get errors for. If omitted, all errors are returned. * * @returns Set of error messages */ getAllErrors(fieldName?: FormField.Name<T>): Set<string>; /** * The first error message (including nested objects) * * @remarks Alias for {@link Validator.firstErrorMessage}. */ get firstErrorMessage(): string | null; /** @internal @ignore */ [internalToken](): { fields: Map<string, FormField>; bindings: Map<string, FormBinding>; submission: Submission; }; } declare namespace Form { /** @inline */ type Handlers = Submission.Handlers; } export { Form, type FormBinding, FormBindingFunc, FormBindingFuncExtension, type FormConfig, FormField, configureForm };