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.

1 lines 35.3 kB
{"version":3,"sources":["../src/form.ts","../src/field.ts","../src/binding.ts","../src/config.ts","../src/submission.ts"],"sourcesContent":["import { action, computed, makeObservable, observable } from \"mobx\";\nimport { v4 as uuidV4 } from \"uuid\";\nimport { Validator, Watcher, StandardNestedFetcher, KeyPath } from \"@mobx-sentinel/core\";\nimport { FormField } from \"./field\";\nimport { FormBinding, FormBindingConstructor, FormBindingFunc, getSafeBindingName } from \"./binding\";\nimport { FormConfig, globalConfig } from \"./config\";\nimport { Submission } from \"./submission\";\n\nconst registry = new WeakMap<object, Map<symbol, Form<any>>>();\nconst defaultFormKey = Symbol(\"form.defaultFormKey\");\nconst internalToken = Symbol(\"form.internalToken\");\n\nexport class Form<T> {\n readonly id = uuidV4();\n readonly #formKey: symbol;\n readonly watcher: Watcher;\n readonly validator: Validator<T>;\n readonly #submission = new Submission();\n readonly #fields = new Map<string, FormField>();\n readonly #bindings = new Map<string, FormBinding>();\n readonly #nestedFetcher: StandardNestedFetcher<Form<any>>;\n readonly #localConfig = observable.box<Partial<FormConfig>>({});\n\n /** Extension fields for bindings */\n [k: `bind${Capitalize<string>}`]: unknown;\n\n /**\n * Get the form instance for a subject.\n *\n * - Returns the existing form instance if the subject is already associated with one.\\\n * Otherwise, creates a new form instance and associates it with the subject.\n * - The form instance is cached in the internal registry,\n * and it will be garbage collected when the subject is no longer in use.\\\n * In rare cases, you may need to manually dispose the form instance using {@link Form.dispose}.\n *\n * @param subject The subject to associate with the form\n * @param formKey The key to associate with the form.\n * If you need to associate multiple forms with the same subject, use different keys.\n *\n * @throws TypeError when the subject is not an object.\n */\n static get<T extends object>(subject: T, formKey?: symbol): Form<T> {\n const form = this.getSafe(subject, formKey);\n if (!form) {\n throw new TypeError(\"subject: Expected an object\");\n }\n return form;\n }\n\n /**\n * Get the form instance for a subject.\n *\n * Same as {@link Form.get} but returns null instead of throwing an error.\n */\n static getSafe<T extends object>(subject: T, formKey?: symbol): Form<T> | null {\n if (!subject || typeof subject !== \"object\") {\n return null;\n }\n\n formKey ??= defaultFormKey;\n\n let map = registry.get(subject);\n if (!map) {\n map = new Map();\n registry.set(subject, map);\n }\n\n let instance = map.get(formKey);\n if (!instance) {\n instance = new this<T>(internalToken, {\n subject,\n formKey,\n });\n map.set(formKey, instance);\n }\n return instance;\n }\n\n /**\n * Manually dispose the form instance for a subject.\n *\n * Use with caution.\\\n * You don't usually need to use this method at all.\\\n * It's only for advanced use cases, such as testing.\n *\n * @see {@link Form.get}\n */\n static dispose(subject: object, formKey?: symbol) {\n const map = registry.get(subject);\n if (!map) return;\n if (formKey) {\n map.delete(formKey);\n } else {\n map.clear();\n }\n }\n\n private constructor(\n token: symbol,\n args: {\n subject: T & object;\n formKey: symbol;\n }\n ) {\n if (token !== internalToken) {\n throw new Error(\"private constructor\");\n }\n\n this.#formKey = args.formKey;\n this.watcher = Watcher.get(args.subject);\n this.validator = Validator.get(args.subject);\n this.#nestedFetcher = new StandardNestedFetcher(args.subject, (entry) => Form.getSafe(entry.data, this.#formKey));\n\n makeObservable(this);\n\n this.#submission.addHandler(\"didSubmit\", (succeed) => {\n if (succeed) {\n this.reset();\n }\n });\n }\n\n /**\n * The configuration of the form\n *\n * This is a computed value that combines the global configuration and the local configuration.\n */\n @computed.struct\n get config(): Readonly<FormConfig> {\n return {\n ...globalConfig,\n ...this.#localConfig.get(),\n };\n }\n\n /** Configure the form locally */\n @action.bound\n configure: {\n /** Override the global configuration locally */\n (config: Partial<Readonly<FormConfig>>): void;\n /** Reset to the global configuration */\n (reset: true): void;\n } = (arg0) => {\n if (typeof arg0 === \"object\") {\n Object.assign(this.#localConfig.get(), arg0);\n } else {\n this.#localConfig.set({});\n }\n };\n\n /** Whether the form is dirty (including sub-forms) */\n get isDirty() {\n return this.watcher.changed;\n }\n\n /** Whether the form is valid (including sub-forms) */\n get isValid() {\n return this.validator.isValid;\n }\n\n /** The number of invalid fields */\n get invalidFieldCount() {\n return this.validator.invalidKeyCount;\n }\n\n /** The number of total invalid field paths (counts invalid fields in sub-forms) */\n get invalidFieldPathCount() {\n return this.validator.invalidKeyPathCount;\n }\n\n /** Whether the form is in validator state */\n get isValidating() {\n return this.validator.isValidating;\n }\n\n /** Whether the form is in submitting state */\n get isSubmitting() {\n return this.#submission.isRunning;\n }\n\n /** Whether the form is busy (submitting or validating) */\n @computed\n get isBusy() {\n return this.isSubmitting || this.isValidating;\n }\n\n /** Whether the form can be submitted */\n @computed\n get canSubmit() {\n return (\n !this.isBusy &&\n (this.config.allowSubmitInvalid || this.isValid) &&\n (this.config.allowSubmitNonDirty || this.isDirty)\n );\n }\n\n /**\n * Sub-forms within the form.\n *\n * Forms are collected via `@nested` annotation.\n */\n get subForms() {\n return this.#nestedFetcher.dataMap;\n }\n\n /** Report error states on all fields and sub-forms */\n @action\n reportError() {\n for (const field of this.#fields.values()) {\n field.reportError();\n }\n for (const entry of this.#nestedFetcher) {\n entry.data.reportError();\n }\n }\n\n /**\n * Reset the form's state\n *\n * It also resets the watcher but not the validator.\n */\n @action\n reset() {\n // NOTE: DO NOT reset the validator here.\n this.watcher.reset();\n for (const field of this.#fields.values()) {\n field.reset();\n }\n for (const entry of this.#nestedFetcher) {\n entry.data.reset();\n }\n }\n\n /** Mark the form as dirty */\n @action\n markAsDirty() {\n this.watcher.assumeChanged();\n }\n\n /**\n * Submit the form.\n *\n * @returns true when the submission succeeded.\n */\n async submit(args?: { force?: boolean }) {\n if (!args?.force && !this.canSubmit) return false;\n return this.#submission.exec();\n }\n\n /**\n * Add a handler to the form\n *\n * @returns A function to remove the handler.\n */\n addHandler: Submission[\"addHandler\"] = (...args) => {\n return this.#submission.addHandler(...args);\n };\n\n /** Get a field by name */\n getField(fieldName: FormField.Name<T>) {\n let field = this.#fields.get(fieldName);\n if (!field) {\n field = new FormField({\n fieldName: String(fieldName),\n validator: this.validator,\n getFinalizationDelayMs: () => this.config.autoFinalizationDelayMs,\n });\n this.#fields.set(fieldName, field);\n }\n return field;\n }\n\n /** Define a binding by key */\n #defineBinding(bindingKey: string, create: () => FormBinding) {\n let binding = this.#bindings.get(bindingKey);\n if (!binding) {\n binding = create();\n this.#bindings.set(bindingKey, binding);\n }\n return binding;\n }\n\n /** Create a binding to the form */\n #bindToForm: FormBindingFunc.ForForm<T> = (\n binding: FormBindingConstructor.ForForm,\n config?: FormBindingFunc.Config\n ) => {\n const key = `${getSafeBindingName(binding)}:${config?.cacheKey}`;\n const instance = this.#defineBinding(key, () => new binding(this, config));\n instance.config = config; // Update on every call\n return instance.props;\n };\n\n /** Create a binding to a field */\n #bindToField: FormBindingFunc.ForField<T> = (\n fieldName: FormField.Name<T>,\n binding: FormBindingConstructor.ForField,\n config?: FormBindingFunc.Config\n ) => {\n const key = `${fieldName}@${getSafeBindingName(binding)}:${config?.cacheKey}`;\n const instance = this.#defineBinding(key, () => {\n const field = this.getField(fieldName);\n return new binding(field, config);\n });\n instance.config = config; // Update on every call\n return instance.props;\n };\n\n /** Create a binding to multiple fields */\n #bindToMultiField: FormBindingFunc.ForMultiField<T> = (\n fieldNames: FormField.Name<T>[],\n binding: FormBindingConstructor.ForMultiField,\n config?: FormBindingFunc.Config\n ) => {\n const key = `${fieldNames.join(\",\")}@${getSafeBindingName(binding)}:${config?.cacheKey}`;\n const instance = this.#defineBinding(key, () => {\n const fields = fieldNames.map((name) => this.getField(name));\n return new binding(fields, config);\n });\n instance.config = config; // Update on every call\n return instance.props;\n };\n\n /** Bind to a field or the form */\n bind: FormBindingFunc<T> = (...args: any[]) => {\n if (typeof args[0] === \"string\") {\n return this.#bindToField(args[0] as any, args[1], args[2]);\n }\n if (Array.isArray(args[0])) {\n return this.#bindToMultiField(args[0], args[1], args[2]);\n }\n return this.#bindToForm(args[0], args[1]);\n };\n\n /**\n * Get the error messages for a field\n *\n * @param fieldName - The field name to get errors for.\n * @param includePreReported - Whether to include errors that are yet to be reported.\n */\n getErrors(fieldName: FormField.Name<T>, includePreReported = false): ReadonlySet<string> {\n const field = this.getField(fieldName);\n\n if (!includePreReported && !field.isErrorReported) {\n return new Set();\n }\n\n // Reading errors from FormField#errors is computed,\n // so notifications only trigger when the error of the specific field changes\n return field.errors;\n }\n\n /**\n * Get all error messages for the form\n *\n * @param fieldName - The field name to get errors for. If omitted, all errors are returned.\n */\n getAllErrors(fieldName?: FormField.Name<T>) {\n return this.validator.getErrorMessages(fieldName ? KeyPath.build(fieldName) : KeyPath.Self, true);\n }\n\n /** The first error message (including nested objects) */\n @computed\n get firstErrorMessage() {\n return this.validator.firstErrorMessage;\n }\n\n /** @internal @ignore */\n [internalToken]() {\n return {\n fields: this.#fields,\n bindings: this.#bindings,\n submission: this.#submission,\n };\n }\n}\n\nexport namespace Form {\n export type Handlers = Submission.Handlers;\n}\n\n/** @internal @ignore */\nexport function debugForm<T>(form: Form<T>) {\n return form[internalToken]();\n}\n","import { action, comparer, computed, makeObservable, observable, reaction } from \"mobx\";\nimport { v4 as uuidV4 } from \"uuid\";\nimport { KeyPath, type Validator } from \"@mobx-sentinel/core\";\n\nconst internalToken = Symbol(\"formField.internal\");\n\nexport class FormField {\n readonly id = uuidV4();\n readonly fieldName: string;\n readonly validator: Validator<any>;\n readonly #getFinalizationDelayMs: () => number;\n readonly #isTouched = observable.box(false);\n readonly #changeType = observable.box<FormField.ChangeType | null>(null);\n readonly #isReported = observable.box(false);\n readonly #isReportedDelayed = observable.box(false);\n #timerId: number | null = null;\n\n /** @ignore */\n constructor(args: { fieldName: string; validator: Validator<any>; getFinalizationDelayMs: () => number }) {\n makeObservable(this);\n this.fieldName = args.fieldName;\n this.validator = args.validator;\n this.#getFinalizationDelayMs = args.getFinalizationDelayMs;\n\n // Delay the error reporting until the validation is up-to-date.\n reaction(\n () => [this.#isReported.get(), this.validator.isValidating] as const,\n ([isReported, isValidating]) => {\n if (!isValidating) {\n this.#isReportedDelayed.set(isReported);\n }\n },\n { equals: comparer.shallow }\n );\n }\n\n /** Whether the field is touched */\n get isTouched() {\n return this.#isTouched.get();\n }\n /**\n * Whether the field value is intermediate (partial input).\n *\n * The input is incomplete and does not yet conform to the expected format.\n * Example: Typing \"user@\" in an email field.\n */\n get isIntermediate() {\n return this.#changeType.get() === \"intermediate\";\n }\n /** Whether the field value is changed */\n get isChanged() {\n return !!this.#changeType.get();\n }\n\n /**\n * Whether the error states has been reported\n *\n * Check this value to determine if errors should be displayed.\n *\n * @returns\n * - 'undefined': Validity of the field is undetermined.\n * - 'false': The field is valid.\n * - 'true': The field is invalid.\n *\n * This distinction is essential for `aria-invalid` attribute.\n */\n @computed\n get isErrorReported() {\n if (!this.#isReportedDelayed.get()) return undefined;\n return this.hasErrors;\n }\n\n /**\n * Error messages for the field\n *\n * Regardless of {@link isErrorReported}, this value is always up-to-date.\n */\n @computed.struct\n get errors(): ReadonlySet<string> {\n return this.validator.getErrorMessages(KeyPath.build(this.fieldName));\n }\n\n /**\n * Whether the field has errors\n *\n * Regardless of {@link isErrorReported}, this value is always up-to-date.\n */\n @computed\n get hasErrors() {\n return this.validator.hasErrors(KeyPath.build(this.fieldName));\n }\n\n /** Reset the field to its initial state */\n @action\n reset() {\n this.#changeType.set(null);\n this.#isTouched.set(false);\n this.#isReported.set(false);\n this.#cancelFinalizeChangeWithDelay();\n }\n\n /** Mark the field as touched (usually triggered by onFocus) */\n @action\n markAsTouched() {\n this.#isTouched.set(true);\n }\n\n /** Mark the field as changed (usually triggered by onChange) */\n @action\n markAsChanged(type: FormField.ChangeType = \"final\") {\n this.#changeType.set(type);\n\n switch (type) {\n case \"final\": {\n this.#cancelFinalizeChangeWithDelay();\n this.reportError();\n break;\n }\n case \"intermediate\": {\n this.#finalizeChangeWithDelay();\n break;\n }\n }\n }\n\n /**\n * Report the errors of the field.\n *\n * It will wait until the validation is up-to-date before reporting the errors.\n */\n @action\n reportError() {\n this.#isReported.set(true);\n }\n\n /** Finalize the intermediate change if needed (usually triggered by onBlur) */\n finalizeChangeIfNeeded() {\n this.#cancelFinalizeChangeWithDelay();\n if (this.isIntermediate) {\n this.markAsChanged(\"final\");\n }\n }\n\n #finalizeChangeWithDelay() {\n this.#cancelFinalizeChangeWithDelay();\n this.#timerId = +setTimeout(() => {\n this.finalizeChangeIfNeeded();\n }, this.#getFinalizationDelayMs());\n }\n\n #cancelFinalizeChangeWithDelay() {\n if (this.#timerId) {\n clearTimeout(this.#timerId);\n this.#timerId = null;\n }\n }\n\n /** @internal @ignore */\n [internalToken]() {\n return {\n isReported: this.#isReported,\n };\n }\n}\n\nexport namespace FormField {\n /** Strict field name */\n export type NameStrict<T> = keyof T & string;\n /** Augmented field name with an arbitrary suffix followed by a colon */\n export type NameAugmented<T> = `${NameStrict<T>}:${string}`;\n /** Field name */\n export type Name<T> = NameStrict<T> | NameAugmented<T>;\n\n /**\n * The type of change that has occurred in the field.\n *\n * - \"final\" - The input is complete and no further update is necessary to make it valid.\n * - \"intermediate\" - The input is incomplete and does not yet conform to the expected format.\n */\n export type ChangeType = \"final\" | \"intermediate\";\n}\n\n/** @internal @ignore */\nexport function debugFormField(field: FormField) {\n return field[internalToken]();\n}\n","import type { Form } from \"./form\";\nimport type { FormField } from \"./field\";\nimport { v4 as uuidV4 } from \"uuid\";\n\ntype ConfigOf<T> = T extends new (form: Form<any>, config: infer Config) => FormBinding\n ? Config\n : T extends new (field: FormField, config: infer Config) => FormBinding\n ? Config\n : T extends new (fields: FormField[], config: infer Config) => FormBinding\n ? Config\n : never;\n\n/** Interface for form binding classes */\nexport interface FormBinding {\n /** Configuration of the binding */\n config?: object;\n /** Binding properties which passed to the view component */\n readonly props: object;\n}\n\n/** Polymorphic constructor of binding classes */\nexport type FormBindingConstructor =\n | FormBindingConstructor.ForField\n | FormBindingConstructor.ForMultiField\n | FormBindingConstructor.ForForm;\nexport namespace FormBindingConstructor {\n /** Constructor for field binding classes */\n export type ForField = new (field: FormField, config?: any) => FormBinding;\n /** Constructor for multi-field binding classes */\n export type ForMultiField = new (fields: FormField[], config?: any) => FormBinding;\n /** Constructor for form binding classes */\n export type ForForm = new (form: Form<any>, config?: any) => FormBinding;\n}\n\n/**\n * Get a safe name for the binding class\n *\n * Since Function.name is vulnerable to minification,\n * a UUID is appended to the name to ensure uniqueness.\n */\nexport function getSafeBindingName(constructor: FormBindingConstructor): string {\n let name = safeBindingNameCache.get(constructor);\n if (!name) {\n name = `${constructor.name}--${uuidV4()}`;\n safeBindingNameCache.set(constructor, name);\n }\n return name;\n}\nconst safeBindingNameCache = new WeakMap<FormBindingConstructor, string>();\n\n/** Polymorphic function of Form#bind */\nexport interface FormBindingFunc<T>\n extends FormBindingFunc.ForField<T>,\n FormBindingFunc.ForMultiField<T>,\n FormBindingFunc.ForForm<T> {}\nexport namespace FormBindingFunc {\n /** Bind configuration */\n export type Config = {\n /** Cache key for the binding */\n cacheKey?: string;\n };\n\n /** Bind to a field */\n export interface ForField<T> {\n /** Create a binding for the field */\n <Binding extends new (field: FormField) => FormBinding>(\n fieldName: FormField.Name<T>,\n binding: Binding,\n config?: Config\n ): InstanceType<Binding>[\"props\"];\n\n /** Create a binding for the field with the config */\n <Binding extends new (field: FormField, config: any) => FormBinding>(\n fieldName: FormField.Name<T>,\n binding: Binding,\n config: NoInfer<ConfigOf<Binding>> & Config\n ): InstanceType<Binding>[\"props\"];\n }\n\n /** Bind to multiple fields */\n export interface ForMultiField<T> {\n /** Create a binding for the multiple fields */\n <Binding extends new (fields: FormField[]) => FormBinding>(\n fieldNames: FormField.Name<T>[],\n binding: Binding,\n config?: Config\n ): InstanceType<Binding>[\"props\"];\n\n /** Create a binding for the multiple fields with the config */\n <Binding extends new (fields: FormField[], config: any) => FormBinding>(\n fieldNames: FormField.Name<T>[],\n binding: Binding,\n config: NoInfer<ConfigOf<Binding>> & Config\n ): InstanceType<Binding>[\"props\"];\n }\n\n /** Bind to the form */\n export interface ForForm<T> {\n /** Create a binding for the form */\n <Binding extends new (form: Form<T>) => FormBinding>(\n binding: Binding,\n config?: Config\n ): InstanceType<Binding>[\"props\"];\n\n /** Create a binding for the form with the config */\n <Binding extends new (form: Form<T>, config: any) => FormBinding>(\n binding: Binding,\n config: NoInfer<ConfigOf<Binding>> & Config\n ): InstanceType<Binding>[\"props\"];\n }\n}\n\nexport namespace FormBindingFuncExtension {\n /** Bind configuration */\n export type Config = FormBindingFunc.Config;\n\n /** Bind to a field */\n export namespace ForField {\n /** Create a binding for the field with an optional config */\n export type OptionalConfig<T, Binding extends new (field: FormField, config?: any) => FormBinding> = (\n fieldName: FormField.Name<T>,\n config?: NoInfer<ConfigOf<Binding>> & Config\n ) => InstanceType<Binding>[\"props\"];\n\n /** Create a binding for the field with a required config */\n export type RequiredConfig<T, Binding extends new (field: FormField, config: any) => FormBinding> = (\n fieldName: FormField.Name<T>,\n config: NoInfer<ConfigOf<Binding>> & Config\n ) => InstanceType<Binding>[\"props\"];\n }\n\n /** Bind to multiple fields */\n export namespace ForMultiField {\n /** Create a binding for the multiple fields */\n export type OptionalConfig<T, Binding extends new (fields: FormField[], config?: any) => FormBinding> = (\n fieldNames: FormField.Name<T>[],\n config?: NoInfer<ConfigOf<Binding>> & Config\n ) => InstanceType<Binding>[\"props\"];\n\n /** Create a binding for the multiple fields with the config */\n export type RequiredConfig<T, Binding extends new (fields: FormField[], config: any) => FormBinding> = (\n fieldNames: FormField.Name<T>[],\n config: NoInfer<ConfigOf<Binding>> & Config\n ) => InstanceType<Binding>[\"props\"];\n }\n\n /** Bind to the form */\n export namespace ForForm {\n /** Create a binding for the form */\n export type OptionalConfig<T, Binding extends new (form: Form<T>, config: any) => FormBinding> = (\n config?: NoInfer<ConfigOf<Binding>> & Config\n ) => InstanceType<Binding>[\"props\"];\n\n /** Create a binding for the form with the config */\n export type RequiredConfig<T, Binding extends new (form: Form<T>, config: any) => FormBinding> = (\n config: NoInfer<ConfigOf<Binding>> & Config\n ) => InstanceType<Binding>[\"props\"];\n }\n}\n","import { observable, runInAction } from \"mobx\";\n\n/** Form configuration */\nexport type FormConfig = {\n /**\n * Automatically finalize the form when the input is intermediate (partial input). [in milliseconds]\n *\n * @default 3000\n */\n autoFinalizationDelayMs: number;\n /**\n * Allow submission even if the form is not dirty.\n *\n * @default false\n */\n allowSubmitNonDirty: boolean;\n /**\n * Allow submission even if the form is invalid.\n *\n * @default false\n */\n allowSubmitInvalid: boolean;\n};\n\n/** Default configuration */\nexport const defaultConfig: Readonly<FormConfig> = Object.freeze({\n autoFinalizationDelayMs: 3000,\n allowSubmitNonDirty: false,\n allowSubmitInvalid: false,\n});\n\n/** Global configuration */\nexport const globalConfig = observable.object(defaultConfig);\n\n/** Update the global configuration */\nexport function configureForm(config: Partial<Readonly<FormConfig>>): Readonly<FormConfig>;\n\n/** Reset the global configuration to the default */\nexport function configureForm(reset: true): Readonly<FormConfig>;\n\nexport function configureForm(config: true | Partial<Readonly<FormConfig>>): Readonly<FormConfig> {\n runInAction(() => {\n if (config === true) {\n Object.assign(globalConfig, defaultConfig);\n } else {\n Object.assign(globalConfig, config);\n }\n });\n return globalConfig;\n}\n","import { observable, runInAction } from \"mobx\";\n\nexport class Submission {\n readonly #isRunning = observable.box(false);\n readonly #handlers: {\n readonly [K in keyof Submission.Handlers]: Set<Submission.Handlers[K]>;\n } = {\n willSubmit: new Set(),\n submit: new Set(),\n didSubmit: new Set(),\n };\n #abortCtrl: AbortController | null = null;\n\n /** Whether the submission is running */\n get isRunning() {\n return this.#isRunning.get();\n }\n\n /** Add a handler for the specific event */\n addHandler<K extends keyof Submission.Handlers>(event: K, handler: Submission.Handlers[K]) {\n this.#handlers[event].add(handler);\n return (): void => void this.#handlers[event].delete(handler);\n }\n\n /** Execute the submission */\n async exec() {\n this.#abortCtrl?.abort();\n const abortCtrl = new AbortController();\n this.#abortCtrl = abortCtrl;\n\n runInAction(() => {\n this.#isRunning.set(true);\n\n try {\n for (const handler of this.#handlers.willSubmit) {\n handler();\n }\n } catch (e) {\n console.warn(e);\n }\n });\n\n let succeed = true;\n try {\n for (const handler of this.#handlers.submit) {\n // Serialized\n if (!(await handler(abortCtrl.signal))) {\n succeed = false;\n break;\n }\n }\n } catch (e) {\n succeed = false;\n console.error(e);\n }\n\n this.#abortCtrl = null;\n runInAction(() => {\n this.#isRunning.set(false);\n\n try {\n for (const handler of this.#handlers.didSubmit) {\n handler(succeed);\n }\n } catch (e) {\n console.warn(e);\n }\n });\n\n return succeed;\n }\n}\n\nexport namespace Submission {\n export type Handlers = {\n willSubmit: () => void;\n submit: (abortSignal: AbortSignal) => Promise<boolean>;\n didSubmit: (succeed: boolean) => void;\n };\n}\n"],"mappings":"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","names":["action","computed","makeObservable","observable","uuidV4","Validator","Watcher","StandardNestedFetcher","KeyPath","action","comparer","computed","makeObservable","observable","reaction","uuidV4","KeyPath","internalToken","FormField","uuidV4","#getFinalizationDelayMs","#isTouched","observable","#changeType","#isReported","#isReportedDelayed","#timerId","args","makeObservable","reaction","isReported","isValidating","comparer","KeyPath","#cancelFinalizeChangeWithDelay","type","#finalizeChangeWithDelay","__decorateClass","computed","action","uuidV4","getSafeBindingName","constructor","name","safeBindingNameCache","observable","runInAction","defaultConfig","globalConfig","configureForm","config","observable","runInAction","Submission","#isRunning","#handlers","#abortCtrl","event","handler","abortCtrl","e","succeed","registry","defaultFormKey","internalToken","_Form","uuidV4","#formKey","#submission","Submission","#fields","#bindings","#nestedFetcher","#localConfig","observable","subject","formKey","form","map","instance","token","args","Watcher","Validator","StandardNestedFetcher","entry","makeObservable","succeed","globalConfig","arg0","field","fieldName","FormField","#defineBinding","bindingKey","create","binding","#bindToForm","config","key","getSafeBindingName","#bindToField","#bindToMultiField","fieldNames","fields","name","includePreReported","KeyPath","__decorateClass","computed","action","Form"]}