@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
TypeScript
import { Validator, Watcher, KeyPath } from '@mobx-sentinel/core';
import * as mobx from 'mobx';
declare const internalToken$1: unique symbol;
declare class FormField {
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 */
[](): {
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 {
/** 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> {
readonly id: string;
readonly watcher: Watcher;
readonly validator: Validator<T>;
/** Extension fields for bindings */
[]: 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 */
[](): {
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 };