UNPKG

@mobx-sentinel/form

Version:

A TypeScript library for non-intrusive model enhancement in MobX applications. Provides model change detection, validation, and form integration capabilities while maintaining the purity of domain models.

326 lines (319 loc) 13.3 kB
import { Validator, Watcher, KeyPath } from '@mobx-sentinel/core'; import * as mobx from 'mobx'; declare const internalToken$1: unique symbol; 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 */ get isTouched(): boolean; /** * Whether the field value is intermediate (partial input). * * The input is incomplete and does not yet conform to the expected format. * Example: Typing "user@" in an email field. */ 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. * * @returns * - 'undefined': Validity of the field is undetermined. * - 'false': The field is valid. * - 'true': The field is invalid. * * This distinction is essential for `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 to its initial state */ reset(): void; /** Mark the field as touched (usually triggered by onFocus) */ markAsTouched(): void; /** Mark the field as changed (usually triggered by onChange) */ 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 followed by a colon */ 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>; declare class Submission { #private; /** Whether the submission is running */ get isRunning(): boolean; /** Add a handler for the specific event */ addHandler<K extends keyof Submission.Handlers>(event: K, handler: Submission.Handlers[K]): () => void; /** Execute the submission */ exec(): Promise<boolean>; } declare namespace Submission { type Handlers = { willSubmit: () => void; submit: (abortSignal: AbortSignal) => Promise<boolean>; didSubmit: (succeed: boolean) => void; }; } declare const internalToken: unique symbol; 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 the form instance for a subject. * * - Returns the existing form instance if the subject is already associated with one.\ * Otherwise, creates a new form instance and associates it with the subject. * - The form instance is cached in the internal registry, * and it will be garbage collected when the subject is no longer in use.\ * In rare cases, you may need to manually dispose the form instance using {@link Form.dispose}. * * @param subject The subject to associate with the form * @param formKey The key to associate with the form. * If you need to associate multiple forms with the same subject, use different keys. * * @throws TypeError when the subject is not an object. */ static get<T extends object>(subject: T, formKey?: symbol): Form<T>; /** * Get the form instance for a 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 a 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) */ get isDirty(): boolean; /** Whether the form is valid (including sub-forms) */ get isValid(): boolean; /** The number of invalid fields */ get invalidFieldCount(): number; /** The number of total invalid field paths (counts invalid fields in sub-forms) */ get invalidFieldPathCount(): number; /** Whether the form is in validator state */ 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 */ 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 * * It also resets the watcher but not the validator. */ reset(): void; /** Mark the form as dirty */ markAsDirty(): void; /** * Submit the form. * * @returns true when the submission succeeded. */ submit(args?: { force?: boolean; }): Promise<boolean>; /** * Add a handler to the form * * @returns A function to remove the handler. */ addHandler: Submission["addHandler"]; /** Get a field by name */ getField(fieldName: FormField.Name<T>): FormField; /** Bind to a field or the form */ bind: FormBindingFunc<T>; /** * Get the error messages for a field * * @param fieldName - The field name to get errors for. * @param includePreReported - Whether to include errors that are yet to be reported. */ getErrors(fieldName: FormField.Name<T>, includePreReported?: boolean): ReadonlySet<string>; /** * Get all error messages for the form * * @param fieldName - The field name to get errors for. If omitted, all errors are returned. */ getAllErrors(fieldName?: FormField.Name<T>): Set<string>; /** The first error message (including nested objects) */ get firstErrorMessage(): string | null; /** @internal @ignore */ [internalToken](): { fields: Map<string, FormField>; bindings: Map<string, FormBinding>; submission: Submission; }; } declare namespace Form { type Handlers = Submission.Handlers; } export { Form, type FormBinding, FormBindingFunc, FormBindingFuncExtension, type FormConfig, FormField, configureForm };