import { batch, createStore } from '@tanstack/store' import { deleteBy, determineFormLevelErrorSourceAndValue, evaluate, functionalUpdate, getAsyncValidatorArray, getBy, getSyncValidatorArray, isFieldInGroup, isGlobalFormValidationError, isNonEmptyArray, mergeOpts, setBy, throttleFormState, uuid, } from './utils' import { defaultValidationLogic } from './ValidationLogic' import { isStandardSchemaValidator, standardSchemaValidators, } from './standardSchemaValidator' import { defaultFieldMeta, metaHelper } from './metaHelper' import { formEventClient } from './EventClient' import type { AnyFieldLikeMeta, AnyFieldLikeMetaBase, ExtractGlobalFormError, FieldInfo, FormLikeAPI, FormValidationError, FormValidationErrorMap, GlobalFormValidationError, ListenerCause, UpdateMetaOptions, ValidationCause, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, } from './types' import type { ReadonlyStore, Store } from '@tanstack/store' // types import type { ValidationLogicFn } from './ValidationLogic' import type { StandardSchemaV1, StandardSchemaV1Issue, TStandardSchemaValidatorValue, } from './standardSchemaValidator' import type { AnyFieldApi } from './FieldApi' import type { AnyFormGroupApi, AnyFormGroupMeta, FormGroupState, } from './FormGroupApi' import type { DeepKeys, DeepKeysOfType, DeepValue, RejectPromiseValidator, } from './util-types' import type { Updater } from './utils' /** * @private */ // TODO: Add the `Unwrap` type to the errors type FormErrorMapFromValidator< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, > = Partial< Record< DeepKeys, ValidationErrorMap< TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > > > export type FormValidateFn = (props: { value: TFormData formApi: FormApi< TFormData, // This is technically an edge-type; which we try to keep non-`any`, but in this case // It's referring to an inaccessible type from the field validate function inner types, so it's not a big deal any, any, any, any, any, any, any, any, any, any, any > }) => unknown /** * @private */ export type FormValidateOrFn = | FormValidateFn | StandardSchemaV1 export type UnwrapFormValidateOrFn< TValidateOrFn extends undefined | FormValidateOrFn, > = [TValidateOrFn] extends [FormValidateFn] ? ExtractGlobalFormError> : [TValidateOrFn] extends [StandardSchemaV1] ? Record : undefined /** * @private */ export type FormValidateAsyncFn = (props: { value: TFormData formApi: FormApi< TFormData, // This is technically an edge-type; which we try to keep non-`any`, but in this case // It's referring to an inaccessible type from the field validate function inner types, so it's not a big deal any, any, any, any, any, any, any, any, any, any, any > signal: AbortSignal }) => unknown | Promise export type FormValidator = { validate(options: { value: TType }, fn: TFn): ValidationError validateAsync( options: { value: TType }, fn: TFn, ): Promise> } type ValidationPromiseResult = | { fieldErrors: Partial, ValidationError>> errorMapKey: ValidationErrorMapKeys } | undefined /** * @private */ export type FormAsyncValidateOrFn = | FormValidateAsyncFn | StandardSchemaV1 export type UnwrapFormAsyncValidateOrFn< TValidateOrFn extends undefined | FormAsyncValidateOrFn, > = [TValidateOrFn] extends [FormValidateAsyncFn] ? ExtractGlobalFormError>> : [TValidateOrFn] extends [StandardSchemaV1] ? Record : undefined export interface FormValidators< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, > { /** * Optional function that fires as soon as the component mounts. */ onMount?: RejectPromiseValidator /** * Optional function that checks the validity of your data whenever a value changes */ onChange?: RejectPromiseValidator /** * Optional onChange asynchronous counterpart to onChange. Useful for more complex validation logic that might involve server requests. */ onChangeAsync?: TOnChangeAsync /** * The default time in milliseconds that if set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds. */ onChangeAsyncDebounceMs?: number /** * Optional function that validates the form data when a field loses focus, returns a `FormValidationError` */ onBlur?: RejectPromiseValidator /** * Optional onBlur asynchronous validation method for when a field loses focus returns a ` FormValidationError` or a promise of `Promise` */ onBlurAsync?: TOnBlurAsync /** * The default time in milliseconds that if set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds. */ onBlurAsyncDebounceMs?: number onSubmit?: RejectPromiseValidator onSubmitAsync?: TOnSubmitAsync onDynamic?: RejectPromiseValidator onDynamicAsync?: TOnDynamicAsync onDynamicAsyncDebounceMs?: number } export interface FormListenersPropsGroup< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta = never, > { formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > groupApi: AnyFormGroupApi } export interface FormListenersPropsField< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta = never, > { formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > fieldApi: AnyFieldApi } export interface FormListeners< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta = never, > { onChange?: ( props: FormListenersPropsField< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) => void onChangeDebounceMs?: number onChangeGroup?: ( props: FormListenersPropsGroup< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) => void onChangeGroupDebounceMs?: number onBlur?: (props: { formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > fieldApi: AnyFieldApi }) => void onBlurDebounceMs?: number onMount?: (props: { formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > }) => void onSubmit?: (props: { formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > meta: TSubmitMeta }) => void onFieldUnmount?: ( props: FormListenersPropsField< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) => void onGroupUnmount?: ( props: FormListenersPropsGroup< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) => void } /** * An object representing the base properties of a form, unrelated to any validators */ export interface BaseFormOptions { /** * Set initial values for your form. */ defaultValues?: TFormData /** * onSubmitMeta, the data passed from the handleSubmit handler, to the onSubmit function props */ onSubmitMeta?: TSubmitMeta } /** * An object representing the options for a form. */ export interface FormOptions< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn, in out TOnChange extends undefined | FormValidateOrFn, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn, in out TOnBlur extends undefined | FormValidateOrFn, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn, in out TOnSubmit extends undefined | FormValidateOrFn, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, in out TOnDynamic extends undefined | FormValidateOrFn, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, in out TOnServer extends undefined | FormAsyncValidateOrFn, in out TSubmitMeta = never, > extends BaseFormOptions { /** * The form name, used for devtools and identification */ formId?: string /** * The default state for the form. */ defaultState?: Partial< FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > > /** * If true, always run async validation, even when sync validation has produced an error. Defaults to undefined. */ asyncAlways?: boolean /** * Optional time in milliseconds if you want to introduce a delay before firing off an async action. */ asyncDebounceMs?: number /** * If true, allows the form to be submitted in an invalid state i.e. canSubmit will remain true regardless of validation errors. Defaults to undefined. */ canSubmitWhenInvalid?: boolean /** * A list of validators to pass to the form */ validators?: FormValidators< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > validationLogic?: ValidationLogicFn /** * form level listeners */ listeners?: FormListeners< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > /** * A function to be called when the form is submitted, what should happen once the user submits a valid form returns `any` or a promise `Promise` */ onSubmit?: (props: { value: TFormData formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > meta: TSubmitMeta }) => any | Promise /** * Specify an action for scenarios where the user tries to submit an invalid form. */ onSubmitInvalid?: (props: { value: TFormData formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > meta: TSubmitMeta }) => void // Only runs in `core` for the first run on the Form, any additional transforms // need to be handled at framework runtime due to complexity of comparison check // and state merging // // Made intentionally type loose to avoid headaches with framework's individual // `useTransform` hooks transform?: (data: unknown) => unknown } export type AnyFormOptions = FormOptions< any, any, any, any, any, any, any, any, any, any, any, any > /** * An object representing the validation metadata for a field. Not intended for public usage. */ export type ValidationMeta = { /** * An abort controller stored in memory to cancel previous async validation attempts. */ lastAbortController: AbortController } /** * An object representing the current state of the form. */ export type BaseFormState< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn, in out TOnChange extends undefined | FormValidateOrFn, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn, in out TOnBlur extends undefined | FormValidateOrFn, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn, in out TOnSubmit extends undefined | FormValidateOrFn, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, in out TOnDynamic extends undefined | FormValidateOrFn, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, in out TOnServer extends undefined | FormAsyncValidateOrFn, > = { /** * The current values of the form fields. */ values: TFormData /** * The error map for the form itself. */ errorMap: ValidationErrorMap< UnwrapFormValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormAsyncValidateOrFn > /** * An internal mechanism used for keeping track of validation logic in a form. */ validationMetaMap: Record /** * A record of field metadata for each field in the form, not including the derived properties, like `errors` and such */ fieldMetaBase: Partial, AnyFieldLikeMetaBase>> /** * A record of submission lifecycle state for each mounted `FormGroupApi`, * keyed by the group's fully-qualified field name. Stored on the form so * group-level state can be read from `FormApi` without having to walk the * mounted group instances. */ formGroupStateBase: Partial> /** * A boolean indicating if the form is currently in the process of being submitted after `handleSubmit` is called. * * Goes back to `false` when submission completes for one of the following reasons: * - the validation step returned errors. * - the `onSubmit` function has completed. * * Note: if you're running async operations in your `onSubmit` function make sure to await them to ensure `isSubmitting` is set to `false` only when the async operation completes. * * This is useful for displaying loading indicators or disabling form inputs during submission. * */ isSubmitting: boolean /** * A boolean indicating if the `onSubmit` function has completed successfully. * * Goes back to `false` at each new submission attempt. * * Note: you can use isSubmitting to check if the form is currently submitting. */ isSubmitted: boolean /** * A boolean indicating if the form or any of its fields are currently validating. */ isValidating: boolean /** * A counter for tracking the number of submission attempts. */ submissionAttempts: number /** * A boolean indicating if the last submission was successful. */ isSubmitSuccessful: boolean /** * @private, used to force a re-evaluation of the form state when options change */ _force_re_eval?: boolean } export type AnyBaseFormState = BaseFormState< any, any, any, any, any, any, any, any, any, any, any > export type DerivedFormState< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn, in out TOnChange extends undefined | FormValidateOrFn, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn, in out TOnBlur extends undefined | FormValidateOrFn, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn, in out TOnSubmit extends undefined | FormValidateOrFn, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, in out TOnDynamic extends undefined | FormValidateOrFn, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, in out TOnServer extends undefined | FormAsyncValidateOrFn, > = { /** * A boolean indicating if the form is currently validating. */ isFormValidating: boolean /** * A boolean indicating if the form is valid. */ isFormValid: boolean /** * The error array for the form itself. */ errors: Array< NonNullable< | UnwrapFormValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormAsyncValidateOrFn > > /** * A boolean indicating if any of the form fields are currently validating. */ isFieldsValidating: boolean /** * A boolean indicating if all the form fields are valid. Evaluates `true` if there are no field errors. */ isFieldsValid: boolean /** * A boolean indicating if any of the form fields have been touched. */ isTouched: boolean /** * A boolean indicating if any of the form fields have been blurred. */ isBlurred: boolean /** * A boolean indicating if any of the form's fields' values have been modified by the user. Evaluates `true` if the user have modified at least one of the fields. Opposite of `isPristine`. */ isDirty: boolean /** * A boolean indicating if none of the form's fields' values have been modified by the user. Evaluates `true` if the user have not modified any of the fields. Opposite of `isDirty`. */ isPristine: boolean /** * A boolean indicating if all of the form's fields are the same as default values. */ isDefaultValue: boolean /** * A boolean indicating if the form and all its fields are valid. Evaluates `true` if there are no errors. */ isValid: boolean /** * A boolean indicating if the form can be submitted based on its current state. */ canSubmit: boolean /** * A record of field metadata for each field in the form. */ fieldMeta: Partial, AnyFieldLikeMeta>> } export interface FormState< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn, in out TOnChange extends undefined | FormValidateOrFn, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn, in out TOnBlur extends undefined | FormValidateOrFn, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn, in out TOnSubmit extends undefined | FormValidateOrFn, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, in out TOnDynamic extends undefined | FormValidateOrFn, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, in out TOnServer extends undefined | FormAsyncValidateOrFn, > extends BaseFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer >, DerivedFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > {} export type AnyFormState = FormState< any, any, any, any, any, any, any, any, any, any, any > function getDefaultFormState< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, >( defaultState: Partial< FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > >, ): BaseFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > { return { values: defaultState.values ?? ({} as never), errorMap: defaultState.errorMap ?? {}, fieldMetaBase: defaultState.fieldMetaBase ?? ({} as never), formGroupStateBase: defaultState.formGroupStateBase ?? {}, isSubmitted: defaultState.isSubmitted ?? false, isSubmitting: defaultState.isSubmitting ?? false, isValidating: defaultState.isValidating ?? false, submissionAttempts: defaultState.submissionAttempts ?? 0, isSubmitSuccessful: defaultState.isSubmitSuccessful ?? false, validationMetaMap: defaultState.validationMetaMap ?? { onChange: undefined, onBlur: undefined, onSubmit: undefined, onMount: undefined, onServer: undefined, onDynamic: undefined, }, } } /** * @public * * A type representing the Form API with all generics set to `any` for convenience. */ export type AnyFormApi = FormApi< any, any, any, any, any, any, any, any, any, any, any, any > interface ValidateOpts { // Useful in FormGroup where validation doesn't update form error map dontUpdateFormErrorMap?: boolean // Filter which field names to validate, useful for FormGroup validation to filter out fields that don't start with the FormGroup name filterFieldNames?: (fieldName: DeepKeys) => boolean // When form-level validators are run on behalf of a `FormGroupApi` (e.g. // because a field inside that group is revalidating), pass the group so // strategies like `revalidateLogic` can gate on the group's own // `submissionAttempts` instead of the parent form's. group?: AnyFormGroupApi } /** * We cannot use methods and must use arrow functions. Otherwise, our React adapters * will break due to loss of the method when using spread. */ /** * A class representing the Form API. It handles the logic and interactions with the form state. * * Normally, you will not need to create a new `FormApi` instance directly. Instead, you will use a framework * hook/function like `useForm` or `createForm` to create a new instance for you that uses your framework's reactivity model. * However, if you need to create a new instance manually, you can do so by calling the `new FormApi` constructor. */ export class FormApi< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn, in out TOnChange extends undefined | FormValidateOrFn, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn, in out TOnBlur extends undefined | FormValidateOrFn, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn, in out TOnSubmit extends undefined | FormValidateOrFn, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, in out TOnDynamic extends undefined | FormValidateOrFn, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, in out TOnServer extends undefined | FormAsyncValidateOrFn, in out TSubmitMeta = never, > implements FormLikeAPI { /** * The options for the form. */ options: FormOptions< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta > = {} baseStore!: Store< BaseFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > > fieldMetaDerived: Store< FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer >['fieldMeta'] > /** * A derived store of every mounted `FormGroupApi`'s `meta`, keyed by * group name. Mirrors `fieldMetaDerived` for fields: per-group `meta` * is computed once on the form (from `baseStore.formGroupStateBase`, * `fieldMetaDerived`, and the registered `formGroupApis`) so reads * from a `FormGroupApi.store` instance stay minimal. */ formGroupMetaDerived!: ReadonlyStore> store: ReadonlyStore< FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > > /** * A record of field information for each field in the form. */ fieldInfo: Partial, FieldInfo>> = {} /** * The set of currently-mounted `FormGroupApi` instances belonging to * this form. Used by `FieldApi.validate` to cascade field-level changes * into the validators of any group that encompasses the field. */ formGroupApis: Set = new Set() get state() { return this.store.state } /** * @private */ timeoutIds: { validations: Record | null> listeners: Record | null> formListeners: Record | null> } /** * @private */ _formId: string /** * @private */ private _devtoolsSubmissionOverride: boolean /** * Constructs a new `FormApi` instance with the given form options. */ constructor( opts?: FormOptions< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) { this.timeoutIds = { validations: {} as Record, listeners: {} as Record, formListeners: {} as Record, } this._formId = opts?.formId ?? uuid() this._devtoolsSubmissionOverride = false let baseStoreVal: BaseFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > = getDefaultFormState({ ...(opts?.defaultState as any), values: opts?.defaultValues ?? opts?.defaultState?.values, isFormValid: true, }) if (opts?.transform) { baseStoreVal = ( opts.transform({ state: baseStoreVal }) as { state: unknown } ).state as never for (const errKey of Object.keys(baseStoreVal.errorMap)) { const errKeyMap = baseStoreVal.errorMap[errKey as never] as | GlobalFormValidationError | undefined if ( errKeyMap === undefined || !isGlobalFormValidationError(errKeyMap) ) { continue } for (const fieldName of Object.keys(errKeyMap.fields)) { const fieldErr = errKeyMap.fields[fieldName] if (fieldErr === undefined) { continue } const existingFieldMeta = baseStoreVal.fieldMetaBase[ fieldName as never ] as AnyFieldLikeMetaBase | undefined baseStoreVal.fieldMetaBase[fieldName as never] = { isTouched: false, isValidating: false, isBlurred: false, isDirty: false, _arrayVersion: 0, _pendingValidationsCount: 0, ...(existingFieldMeta ?? {}), errorSourceMap: { ...(existingFieldMeta?.['errorSourceMap'] ?? {}), onChange: 'form', }, errorMap: { ...(existingFieldMeta?.['errorMap'] ?? {}), [errKey as never]: fieldErr, }, } satisfies AnyFieldLikeMetaBase as never } } } this.baseStore = createStore(baseStoreVal) as never let prevBaseStore: | BaseFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > | undefined = undefined this.fieldMetaDerived = createStore( (prevVal: Record, AnyFieldLikeMeta> | undefined) => { const currBaseStore = this.baseStore.get() let originalMetaCount = 0 const fieldMeta: FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer >['fieldMeta'] = {} for (const fieldName of Object.keys( currBaseStore.fieldMetaBase, ) as Array) { const currBaseMeta = currBaseStore.fieldMetaBase[ fieldName as never ] as AnyFieldLikeMetaBase const prevBaseMeta = prevBaseStore?.fieldMetaBase[ fieldName as never ] as AnyFieldLikeMetaBase | undefined const prevFieldInfo = prevVal?.[fieldName as never as keyof typeof prevVal] const curFieldVal = getBy(currBaseStore.values, fieldName) let fieldErrors = prevFieldInfo?.errors if ( !prevBaseMeta || currBaseMeta.errorMap !== prevBaseMeta.errorMap ) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition fieldErrors = Object.values(currBaseMeta.errorMap ?? {}).filter( (val) => val !== undefined, ) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const fieldInstance = this.getFieldInfo(fieldName)?.instance if (!fieldInstance || !fieldInstance.options.disableErrorFlat) { fieldErrors = fieldErrors.flat(1) } } // As primitives, we don't need to aggressively persist the same referential value for performance reasons const isFieldValid = !isNonEmptyArray(fieldErrors) const isFieldPristine = !currBaseMeta.isDirty const isDefaultValue = evaluate( curFieldVal, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.getFieldInfo(fieldName)?.instance?.options.defaultValue ?? getBy(this.options.defaultValues, fieldName), ) if ( prevFieldInfo && prevFieldInfo.isPristine === isFieldPristine && prevFieldInfo.isValid === isFieldValid && prevFieldInfo.isDefaultValue === isDefaultValue && prevFieldInfo.errors === fieldErrors && currBaseMeta === prevBaseMeta ) { fieldMeta[fieldName] = prevFieldInfo originalMetaCount++ continue } fieldMeta[fieldName] = { ...currBaseMeta, errors: fieldErrors ?? [], isPristine: isFieldPristine, isValid: isFieldValid, isDefaultValue: isDefaultValue, } satisfies AnyFieldLikeMeta as AnyFieldLikeMeta } if (!Object.keys(currBaseStore.fieldMetaBase).length) return fieldMeta if ( prevVal && originalMetaCount === Object.keys(currBaseStore.fieldMetaBase).length ) { return prevVal } prevBaseStore = this.baseStore.get() return fieldMeta }, ) as never this.formGroupMetaDerived = createStore>( (prevVal) => { const currBaseStore = this.baseStore.get() const currFieldMeta = this.fieldMetaDerived.get() as Record< string, AnyFieldLikeMeta | undefined > const result: Record = {} for (const group of this.formGroupApis) { const groupName = group.name as string const lifecycle = currBaseStore.formGroupStateBase[groupName] ?? ({ isSubmitted: false, isSubmitting: false, isValidating: false, submissionAttempts: 0, isSubmitSuccessful: false, } as FormGroupState) // The group's own field-meta entry (its `errorMap` is where the // group's own validators write via `setFieldMeta`). const ownFieldMeta = currFieldMeta[groupName] as | AnyFieldLikeMeta | undefined // Aggregate validity / interaction flags across descendant fields // (excluding the group's own self-entry — its validity is tracked // separately via `isGroupValid`). let isFieldsValidating = false let isFieldsValid = true let aggIsTouched = false let aggIsBlurred = false let aggIsDefaultValue = true let aggIsDirty = false for (const fieldName in currFieldMeta) { if (fieldName === groupName) continue if (!isFieldInGroup(groupName, fieldName)) continue const m = currFieldMeta[fieldName] if (!m) continue if (m.isValidating) isFieldsValidating = true if (!m.isValid) isFieldsValid = false if (m.isTouched) aggIsTouched = true if (m.isBlurred) aggIsBlurred = true if (!m.isDefaultValue) aggIsDefaultValue = false if (m.isDirty) aggIsDirty = true } const isPristine = !aggIsDirty const isValidating = !!isFieldsValidating || lifecycle.isValidating // Group's own errors derived from `ownFieldMeta.errorMap`. Mirrors // the per-instance reduction that previously lived in // `FormGroupApi.store`. Standard-schema group validators may // return `{ group, fields }`; we only surface the `group` portion // here (the `fields` portion is fanned out onto child fields by // `distributeFieldErrors`). const errorMap = ownFieldMeta?.errorMap ?? {} const errorSourceMap = ownFieldMeta?.errorSourceMap ?? {} const hasOnMountError = Boolean( errorMap.onMount || Object.entries(currFieldMeta).some( ([fieldName, field]) => field && fieldName !== groupName && isFieldInGroup(groupName, fieldName) && field.errorMap.onMount, ), ) const prevGroupMeta = prevVal?.[groupName] let errors = prevGroupMeta?.errors ?? [] if ( !prevGroupMeta || (prevGroupMeta as unknown as { __srcErrorMap?: unknown }) .__srcErrorMap !== errorMap ) { errors = Object.values(errorMap).reduce((acc, curr) => { if (curr === undefined) return acc if ( curr && typeof curr === 'object' && 'fields' in (curr as object) ) { const groupErr = (curr as { group?: unknown }).group if (groupErr !== undefined) acc.push(groupErr) return acc } acc.push(curr) return acc }, []) } const isGroupValid = errors.length === 0 const isValid = isFieldsValid && isGroupValid const submitInvalid = group.options.canSubmitWhenInvalid ?? false const canSubmit = (lifecycle.submissionAttempts === 0 && !aggIsTouched && !hasOnMountError) || (!isValidating && !lifecycle.isSubmitting && isValid) || submitInvalid // Reuse previous identity when shallow-equal — keeps subscribers // from re-rendering when nothing meaningful changed. if ( prevGroupMeta && prevGroupMeta.errorMap === errorMap && prevGroupMeta.errorSourceMap === errorSourceMap && prevGroupMeta.errors === errors && prevGroupMeta.isFieldsValidating === isFieldsValidating && prevGroupMeta.isFieldsValid === isFieldsValid && prevGroupMeta.isGroupValid === isGroupValid && prevGroupMeta.isValid === isValid && prevGroupMeta.canSubmit === canSubmit && prevGroupMeta.isTouched === aggIsTouched && prevGroupMeta.isBlurred === aggIsBlurred && prevGroupMeta.isPristine === isPristine && prevGroupMeta.isDefaultValue === aggIsDefaultValue && prevGroupMeta.isDirty === aggIsDirty && prevGroupMeta.isValidating === isValidating && prevGroupMeta.isSubmitting === lifecycle.isSubmitting && prevGroupMeta.isSubmitted === lifecycle.isSubmitted && prevGroupMeta.submissionAttempts === lifecycle.submissionAttempts && prevGroupMeta.isSubmitSuccessful === lifecycle.isSubmitSuccessful ) { result[groupName] = prevGroupMeta continue } const meta = { // Submission lifecycle (spread first; `isValidating` below // intentionally overrides `lifecycle.isValidating` with the // OR of group-level + descendant-field validating). ...lifecycle, // Field-meta-base fields (so `setMeta` updates can roundtrip // through `state.meta`). errorMap, errorSourceMap, _arrayVersion: ownFieldMeta?._arrayVersion ?? 0, // Aggregated descendant booleans (override field-level meaning // for groups — a group's "field" itself never receives input). isTouched: aggIsTouched, isBlurred: aggIsBlurred, isDirty: aggIsDirty, isPristine, isDefaultValue: aggIsDefaultValue, // Aggregated validity isValid, errors, isValidating, // Group-only flags isFieldsValidating, isFieldsValid, isGroupValid, canSubmit, } as AnyFormGroupMeta // Stash the source errorMap on a non-enumerable shadow so the // next derive run can short-circuit `errors` recomputation when // nothing changed (mirrors `prevMeta.errorMap` tracking on the // old per-instance store). Object.defineProperty(meta, '__srcErrorMap', { value: errorMap, enumerable: false, configurable: true, }) result[groupName] = meta } return result }, ) let prevBaseStoreForStore: | BaseFormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > | undefined = undefined this.store = createStore< FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > >((prevVal) => { const currBaseStore = this.baseStore.get() const currFieldMeta = this.fieldMetaDerived.get() // Computed state const fieldMetaValues = Object.values(currFieldMeta).filter( Boolean, ) as AnyFieldLikeMeta[] const isFieldsValidating = fieldMetaValues.some( (field) => field.isValidating, ) const isFieldsValid = fieldMetaValues.every((field) => field.isValid) const isTouched = fieldMetaValues.some((field) => field.isTouched) const isBlurred = fieldMetaValues.some((field) => field.isBlurred) const isDefaultValue = fieldMetaValues.every( (field) => field.isDefaultValue, ) const shouldInvalidateOnMount = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition isTouched && currBaseStore.errorMap?.onMount const isDirty = fieldMetaValues.some((field) => field.isDirty) const isPristine = !isDirty const hasOnMountError = Boolean( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition currBaseStore.errorMap?.onMount || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition fieldMetaValues.some((f) => f?.errorMap?.onMount), ) const isValidating = !!isFieldsValidating // As `errors` is not a primitive, we need to aggressively persist the same referencial value for performance reasons let errors = prevVal?.errors ?? [] if ( !prevBaseStoreForStore || currBaseStore.errorMap !== prevBaseStoreForStore.errorMap ) { errors = Object.values(currBaseStore.errorMap).reduce< Array< NonNullable< | UnwrapFormValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormAsyncValidateOrFn > > >((prev, curr) => { if (curr === undefined) return prev if (curr && isGlobalFormValidationError(curr)) { prev.push(curr.form as never) return prev } prev.push(curr as never) return prev }, []) } const isFormValid = errors.length === 0 const isValid = isFieldsValid && isFormValid const submitInvalid = this.options.canSubmitWhenInvalid ?? false const canSubmit = (currBaseStore.submissionAttempts === 0 && !isTouched && !hasOnMountError) || (!isValidating && !currBaseStore.isSubmitting && isValid) || submitInvalid let errorMap = currBaseStore.errorMap if (shouldInvalidateOnMount) { errors = errors.filter((err) => err !== currBaseStore.errorMap.onMount) errorMap = Object.assign(errorMap, { onMount: undefined }) } if ( prevVal && prevBaseStoreForStore && prevVal.errorMap === errorMap && prevVal.fieldMeta === this.fieldMetaDerived.state && prevVal.errors === errors && prevVal.isFieldsValidating === isFieldsValidating && prevVal.isFieldsValid === isFieldsValid && prevVal.isFormValid === isFormValid && prevVal.isValid === isValid && prevVal.canSubmit === canSubmit && prevVal.isTouched === isTouched && prevVal.isBlurred === isBlurred && prevVal.isPristine === isPristine && prevVal.isDefaultValue === isDefaultValue && prevVal.isDirty === isDirty && evaluate(prevBaseStoreForStore, currBaseStore) ) { return prevVal } const state = { ...currBaseStore, errorMap, fieldMeta: this.fieldMetaDerived.state, errors, isFieldsValidating, isFieldsValid, isFormValid, isValid, canSubmit, isTouched, isBlurred, isPristine, isDefaultValue, isDirty, } as FormState< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer > prevBaseStoreForStore = this.baseStore.get() return state }) this.handleSubmit = this.handleSubmit.bind(this) this.update(opts || {}) } get formId(): string { return this._formId } /** * @private */ runValidator< TValue extends TStandardSchemaValidatorValue & { formApi: AnyFormApi }, TType extends 'validate' | 'validateAsync', >(props: { validate: TType extends 'validate' ? FormValidateOrFn : FormAsyncValidateOrFn value: TValue type: TType }): unknown { if (isStandardSchemaValidator(props.validate)) { return standardSchemaValidators[props.type]( props.value, props.validate, ) as never } return (props.validate as FormValidateFn)(props.value) as never } mount = () => { // devtool broadcasts const cleanupDevtoolBroadcast = this.store.subscribe(() => { throttleFormState(this) }) // devtool requests const cleanupFormStateListener = formEventClient.on( 'request-form-state', (e) => { if (e.payload.id === this._formId) { formEventClient.emit('form-api', { id: this._formId, state: this.store.state, options: this.options, }) } }, ) const cleanupFormResetListener = formEventClient.on( 'request-form-reset', (e) => { if (e.payload.id === this._formId) { this.reset() } }, ) const cleanupFormForceSubmitListener = formEventClient.on( 'request-form-force-submit', (e) => { if (e.payload.id === this._formId) { this._devtoolsSubmissionOverride = true this.handleSubmit() this._devtoolsSubmissionOverride = false } }, ) const cleanup = () => { cleanupFormForceSubmitListener() cleanupFormResetListener() cleanupFormStateListener() cleanupDevtoolBroadcast.unsubscribe() // broadcast form unmount for devtools formEventClient.emit('form-unmounted', { id: this._formId, }) } this.options.listeners?.onMount?.({ formApi: this }) const { onMount } = this.options.validators || {} // broadcast form state for devtools on mounting formEventClient.emit('form-api', { id: this._formId, state: this.store.state, options: this.options, }) // if no validation skip if (!onMount) return cleanup // validate this.validateSync('mount') return cleanup } /** * Updates the form options and form state. */ update = ( options?: FormOptions< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) => { if (!options) return const oldOptions = this.options // Options need to be updated first so that when the store is updated, the state is correct for the derived state this.options = options const shouldUpdateValues = options.defaultValues && !evaluate(options.defaultValues, oldOptions.defaultValues) && !this.state.isTouched const shouldUpdateState = !evaluate(options.defaultState, oldOptions.defaultState) && !this.state.isTouched if (!shouldUpdateValues && !shouldUpdateState) return batch(() => { this.baseStore.setState(() => getDefaultFormState( Object.assign( {}, this.state as any, shouldUpdateState ? options.defaultState : {}, shouldUpdateValues ? { values: options.defaultValues, } : {}, ), ), ) }) if (shouldUpdateValues) { const helper = metaHelper(this) for (const fieldKey of Object.keys( this.fieldInfo, ) as DeepKeys[]) { if (Array.isArray(this.getFieldValue(fieldKey))) { helper.bumpArrayVersion(fieldKey) } } } formEventClient.emit('form-api', { id: this._formId, state: this.store.state, options: this.options, }) } /** * Resets the form state to the default values. * If values are provided, the form will be reset to those values instead and the default values will be updated. * * @param values - Optional values to reset the form to. * @param opts - Optional options to control the reset behavior. */ reset = (values?: TFormData, opts?: { keepDefaultValues?: boolean }) => { const { fieldMeta: currentFieldMeta } = this.state const fieldMetaBase = this.resetFieldMeta(currentFieldMeta) if (values && !opts?.keepDefaultValues) { this.options = { ...this.options, defaultValues: values, } } this.baseStore.setState(() => { let nextValues = values ?? this.options.defaultValues ?? this.options.defaultState?.values if (!values) { ;(Object.values(this.fieldInfo) as FieldInfo[]).forEach( (fieldInfo) => { if ( fieldInfo.instance && fieldInfo.instance.options.defaultValue !== undefined ) { nextValues = setBy( nextValues, fieldInfo.instance.name, fieldInfo.instance.options.defaultValue, ) } }, ) } return getDefaultFormState({ ...(this.options.defaultState as any), values: nextValues, fieldMetaBase, }) }) } /** * Validates all fields according to the FIELD level validators. * This will ignore FORM level validators, use form.validate({ValidationCause}) for a complete validation */ validateAllFields = async (cause: ValidationCause) => { const fieldValidationPromises: Promise[] = [] as any batch(() => { void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( (field) => { if (!field.instance) return const fieldInstance = field.instance // Validate the field fieldValidationPromises.push( // Remember, `validate` is either a sync operation or a promise Promise.resolve().then(() => fieldInstance.validate(cause, { skipFormValidation: true, skipGroupValidation: true, }), ), ) // If any fields are not touched if (!field.instance.store.state.meta.isTouched) { // Mark them as touched field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) } }, ) }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() } /** * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. */ validateArrayFieldsStartingFrom = async < TField extends DeepKeysOfType, >( field: TField, index: number, cause: ValidationCause, ) => { const currentValue = this.getFieldValue(field) const lastIndex = Array.isArray(currentValue) ? Math.max((currentValue as Array).length - 1, 0) : null // We have to validate all fields that have shifted (at least the current field) const fieldKeysToValidate = [`${field}[${index}]`] for (let i = index + 1; i <= (lastIndex ?? 0); i++) { fieldKeysToValidate.push(`${field}[${i}]`) } // We also have to include all fields that are nested in the shifted fields const fieldsToValidate = Object.keys(this.fieldInfo).filter((fieldKey) => fieldKeysToValidate.some((key) => fieldKey.startsWith(key)), ) as DeepKeys[] // Validate the fields const fieldValidationPromises: Promise[] = [] as any batch(() => { fieldsToValidate.forEach((nestedField) => { fieldValidationPromises.push( Promise.resolve().then(() => this.validateField(nestedField, cause)), ) }) }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() } /** * Validates a specified field in the form using the correct handlers for a given validation type. */ validateField = >( field: TField, cause: ValidationCause, ) => { const fieldInstance = this.fieldInfo[field]?.instance if (!fieldInstance) { const { hasErrored } = this.validateSync(cause) if (hasErrored && !this.options.asyncAlways) { return this.getFieldMeta(field)?.errors ?? [] } return this.validateAsync(cause).then(() => { return this.getFieldMeta(field)?.errors ?? [] }) } // If the field is not touched (same logic as in validateAllFields) if (!fieldInstance.store.state.meta.isTouched) { // Mark it as touched fieldInstance.setMeta((prev) => ({ ...prev, isTouched: true })) } return fieldInstance.validate(cause) } /** * TODO: This code is copied from FieldApi, we should refactor to share * @private */ validateSync = ( cause: ValidationCause, validateOpts?: ValidateOpts, ): { hasErrored: boolean fieldsErrorMap: FormErrorMapFromValidator< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > } => { const validates = getSyncValidatorArray(cause, { ...this.options, form: this, group: validateOpts?.group, validationLogic: this.options.validationLogic || defaultValidationLogic, }) let hasErrored = false as boolean // This map will only include fields that have errors in the current validation cycle const currentValidationErrorMap: FormErrorMapFromValidator< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > = {} batch(() => { for (const validateObj of validates) { if (!validateObj.validate) continue const rawError = this.runValidator({ validate: validateObj.validate, value: { value: this.state.values, formApi: this, validationSource: 'form', }, type: 'validate', }) const { formError, fieldErrors } = normalizeError(rawError) const errorMapKey = getErrorMapKey(validateObj.cause) let allFieldsToProcess = new Set([ ...Object.keys(this.state.fieldMeta), ...Object.keys(fieldErrors || {}), ] as DeepKeys[]) if (validateOpts?.filterFieldNames) { allFieldsToProcess = new Set( [...allFieldsToProcess].filter(validateOpts.filterFieldNames), ) } for (const field of allFieldsToProcess) { if ( this.baseStore.state.fieldMetaBase[field] === undefined && !fieldErrors?.[field] ) { continue } const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta const { errorMap: currentErrorMap, errorSourceMap: currentErrorMapSource, } = fieldMeta const newFormValidatorError = fieldErrors?.[field] const { newErrorValue, newSource } = determineFormLevelErrorSourceAndValue({ newFormValidatorError, isPreviousErrorFromFormValidator: // These conditional checks are required, otherwise we get runtime errors. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition currentErrorMapSource?.[errorMapKey] === 'form', // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition previousErrorValue: currentErrorMap?.[errorMapKey], }) if (newSource === 'form') { currentValidationErrorMap[field] = { ...currentValidationErrorMap[field], [errorMapKey]: newFormValidatorError, } } // This conditional check is required, otherwise we get runtime errors. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (currentErrorMap?.[errorMapKey] !== newErrorValue) { this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { ...prev.errorSourceMap, [errorMapKey]: newSource, }, })) } } if (!validateOpts?.dontUpdateFormErrorMap) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.state.errorMap?.[errorMapKey] !== formError) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: formError, }, })) } } if (formError || fieldErrors) { hasErrored = true } } if (validateOpts?.dontUpdateFormErrorMap) { return } /** * when we have an error for onSubmit in the state, we want * to clear the error as soon as the user enters a valid value in the field */ const submitErrKey = getErrorMapKey('submit') if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.errorMap?.[submitErrKey] && cause !== 'submit' && !hasErrored ) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [submitErrKey]: undefined, }, })) } /** * when we have an error for onServer in the state, we want * to clear the error as soon as the user enters a valid value in the field */ const serverErrKey = getErrorMapKey('server') if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.errorMap?.[serverErrKey] && cause !== 'server' && !hasErrored ) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [serverErrKey]: undefined, }, })) } }) return { hasErrored, fieldsErrorMap: currentValidationErrorMap } } /** * @private */ validateAsync = async ( cause: ValidationCause, validateOpts?: ValidateOpts, ): Promise< FormErrorMapFromValidator< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > > => { const validates = getAsyncValidatorArray(cause, { ...this.options, form: this, group: validateOpts?.group, validationLogic: this.options.validationLogic || defaultValidationLogic, }) if (!this.state.isFormValidating) { this.baseStore.setState((prev) => ({ ...prev, isFormValidating: true })) } /** * We have to use a for loop and generate our promises this way, otherwise it won't be sync * when there are no validators needed to be run */ const promises: Promise>[] = [] let fieldErrorsFromFormValidators: | Partial, ValidationError>> | undefined for (const validateObj of validates) { if (!validateObj.validate) continue const key = getErrorMapKey(validateObj.cause) const fieldValidatorMeta = this.state.validationMetaMap[key] fieldValidatorMeta?.lastAbortController.abort() const controller = new AbortController() this.state.validationMetaMap[key] = { lastAbortController: controller, } promises.push( new Promise>(async (resolve) => { let rawError!: | ValidationError | FormValidationError | undefined try { rawError = await new Promise((rawResolve, rawReject) => { setTimeout(async () => { if (controller.signal.aborted) return rawResolve(undefined) try { rawResolve( await this.runValidator({ validate: validateObj.validate!, value: { value: this.state.values, formApi: this, validationSource: 'form', signal: controller.signal, }, type: 'validateAsync', }), ) } catch (e) { rawReject(e) } }, validateObj.debounceMs) }) } catch (e: unknown) { rawError = e as ValidationError } const { formError, fieldErrors: fieldErrorsFromNormalizeError } = normalizeError(rawError) if (fieldErrorsFromNormalizeError) { fieldErrorsFromFormValidators = fieldErrorsFromFormValidators ? { ...fieldErrorsFromFormValidators, ...fieldErrorsFromNormalizeError, } : fieldErrorsFromNormalizeError } const errorMapKey = getErrorMapKey(validateObj.cause) const allFieldsToProcess = new Set([ ...Object.keys(this.state.fieldMeta), ...Object.keys(fieldErrorsFromFormValidators || {}), ] as DeepKeys[]) let fields: DeepKeys[] = Array.from(allFieldsToProcess) if (validateOpts?.filterFieldNames) { fields = fields.filter(validateOpts.filterFieldNames) } for (const field of fields) { if ( this.baseStore.state.fieldMetaBase[field] === undefined && !fieldErrorsFromFormValidators?.[field] ) { continue } const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta const { errorMap: currentErrorMap, errorSourceMap: currentErrorMapSource, } = fieldMeta const newFormValidatorError = fieldErrorsFromFormValidators?.[field] const { newErrorValue, newSource } = determineFormLevelErrorSourceAndValue({ newFormValidatorError, isPreviousErrorFromFormValidator: // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition currentErrorMapSource?.[errorMapKey] === 'form', // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition previousErrorValue: currentErrorMap?.[errorMapKey], }) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (currentErrorMap?.[errorMapKey] !== newErrorValue) { this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { ...prev.errorSourceMap, [errorMapKey]: newSource, }, })) } } if (!validateOpts?.dontUpdateFormErrorMap) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: formError, }, })) } resolve( fieldErrorsFromFormValidators ? { fieldErrors: fieldErrorsFromFormValidators, errorMapKey } : undefined, ) }), ) } let results: ValidationPromiseResult[] = [] const fieldsErrorMap: FormErrorMapFromValidator< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > = {} if (promises.length) { results = await Promise.all(promises) for (const fieldValidationResult of results) { if (fieldValidationResult?.fieldErrors) { const { errorMapKey } = fieldValidationResult for (const [field, fieldError] of Object.entries( fieldValidationResult.fieldErrors, )) { const oldErrorMap = fieldsErrorMap[field as DeepKeys] || {} const newErrorMap = { ...oldErrorMap, [errorMapKey]: fieldError, } fieldsErrorMap[field as DeepKeys] = newErrorMap } } } } this.baseStore.setState((prev) => ({ ...prev, isFormValidating: false, })) return fieldsErrorMap } /** * @private */ validate = ( cause: ValidationCause, validateOpts?: ValidateOpts, ): | FormErrorMapFromValidator< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > | Promise< FormErrorMapFromValidator< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > > => { // Attempt to sync validate first const { hasErrored, fieldsErrorMap } = this.validateSync( cause, validateOpts, ) if (hasErrored && !this.options.asyncAlways) { return fieldsErrorMap } // No error? Attempt async validation return this.validateAsync(cause, validateOpts) } // Needs to edgecase in the React adapter specifically to avoid type errors handleSubmit(): Promise handleSubmit(submitMeta: TSubmitMeta): Promise handleSubmit(submitMeta?: TSubmitMeta): Promise { return this._handleSubmit(submitMeta) } /** * Handles the form submission, performs validation, and calls the appropriate onSubmit or onSubmitInvalid callbacks. */ _handleSubmit = async (submitMeta?: TSubmitMeta): Promise => { this.baseStore.setState((old) => ({ ...old, // Submission attempts mark the form as not submitted isSubmitted: false, // Count submission attempts submissionAttempts: old.submissionAttempts + 1, isSubmitSuccessful: false, // Reset isSubmitSuccessful at the start of submission })) batch(() => { void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( (field) => { if (!field.instance) return // If any fields are not touched if (!field.instance.store.state.meta.isTouched) { // Mark them as touched field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) } }, ) }) const submitMetaArg = submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta) if (!this.state.canSubmit && !this._devtoolsSubmissionOverride) { // On re-submission (submissionAttempts > 1), skip the early return so // validateAllFields can re-run and clear stale field errors (e.g. from a // previous onBlur validation that is no longer relevant). The // isFieldsValid check below will call onSubmitInvalid if the form is // still invalid after re-validation. if (this.baseStore.state.submissionAttempts <= 1) { this.options.onSubmitInvalid?.({ value: this.state.values, formApi: this, meta: submitMetaArg, }) return } } this.baseStore.setState((d) => ({ ...d, isSubmitting: true })) const done = () => { this.baseStore.setState((prev) => ({ ...prev, isSubmitting: false })) } await this.validateAllFields('submit') if (!this.state.isFieldsValid) { done() this.options.onSubmitInvalid?.({ value: this.state.values, formApi: this, meta: submitMetaArg, }) formEventClient.emit('form-submission', { id: this._formId, submissionAttempt: this.state.submissionAttempts, successful: false, stage: 'validateAllFields', errors: (Object.values(this.state.fieldMeta) as AnyFieldLikeMeta[]) .map((meta) => meta.errors) .flat(), }) return } await this.validate('submit') // Fields are invalid, do not submit if (!this.state.isValid) { done() this.options.onSubmitInvalid?.({ value: this.state.values, formApi: this, meta: submitMetaArg, }) formEventClient.emit('form-submission', { id: this._formId, submissionAttempt: this.state.submissionAttempts, successful: false, stage: 'validate', errors: this.state.errors, }) return } batch(() => { void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( (field) => { field.instance?.triggerOnSubmitListener() }, ) }) this.options.listeners?.onSubmit?.({ formApi: this, meta: submitMetaArg }) try { // Run the submit code await this.options.onSubmit?.({ value: this.state.values, formApi: this, meta: submitMetaArg, }) batch(() => { this.baseStore.setState((prev) => ({ ...prev, isSubmitted: true, isSubmitSuccessful: true, // Set isSubmitSuccessful to true on successful submission })) formEventClient.emit('form-submission', { id: this._formId, submissionAttempt: this.state.submissionAttempts, successful: true, }) done() }) } catch (err) { this.baseStore.setState((prev) => ({ ...prev, isSubmitSuccessful: false, // Ensure isSubmitSuccessful is false if an error occurs })) formEventClient.emit('form-submission', { id: this._formId, submissionAttempt: this.state.submissionAttempts, successful: false, stage: 'inflight', onError: err, }) done() throw err } } /** * Gets the value of the specified field. */ getFieldValue = >( field: TField, ): DeepValue => getBy(this.state.values, field) /** * Gets the metadata of the specified field. */ getFieldMeta = >( field: TField, ): AnyFieldLikeMeta | undefined => { return this.state.fieldMeta[field] } /** * Gets the derived `meta` of the form group registered at the given * name. Mirrors `getFieldMeta` for fields. Returns `undefined` if no * `FormGroupApi` with that name is currently mounted. */ getFormGroupMeta = (name: string): AnyFormGroupMeta | undefined => { return this.formGroupMetaDerived.state[name] } /** * Gets the field info of the specified field. */ getFieldInfo = >( field: TField, ): FieldInfo => { return (this.fieldInfo[field] ||= { instance: null, validationMetaMap: { onChange: undefined, onBlur: undefined, onSubmit: undefined, onMount: undefined, onServer: undefined, onDynamic: undefined, }, }) } /** * Updates the metadata of the specified field. */ setFieldMeta = >( field: TField, updater: Updater, ) => { this.baseStore.setState((prev) => { return { ...prev, fieldMetaBase: { ...prev.fieldMetaBase, [field]: functionalUpdate( updater, prev.fieldMetaBase[field] as never, ), }, } }) } /** * resets every field's meta */ resetFieldMeta = >( fieldMeta: Partial>, ): Partial> => { return Object.keys(fieldMeta).reduce( (acc, key) => { const fieldKey = key as TField acc[fieldKey] = defaultFieldMeta return acc }, {} as Partial>, ) } /** * Sets the value of the specified field and optionally updates the touched state. */ setFieldValue = >( field: TField, updater: Updater>, opts?: UpdateMetaOptions, ) => { const dontUpdateMeta = opts?.dontUpdateMeta ?? false const dontRunListeners = opts?.dontRunListeners ?? false const dontValidate = opts?.dontValidate ?? false batch(() => { if (!dontUpdateMeta) { this.setFieldMeta(field, (prev) => ({ ...prev, isTouched: true, isDirty: true, errorMap: { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition ...prev?.errorMap, onMount: undefined, }, })) } this.baseStore.setState((prev) => { return { ...prev, values: setBy(prev.values, field, updater), } }) }) if (!dontRunListeners) { this.getFieldInfo(field).instance?.triggerOnChangeListener() } if (!dontValidate) { this.validateField(field, 'change') } } deleteField = >(field: TField) => { const subFieldsToDelete = Object.keys(this.fieldInfo).filter((f) => { const fieldStr = field.toString() return f !== fieldStr && f.startsWith(fieldStr) }) const fieldsToDelete = [...subFieldsToDelete, field] // Cleanup the last fields this.baseStore.setState((prev) => { const newState = { ...prev } fieldsToDelete.forEach((f) => { newState.values = deleteBy(newState.values, f) delete this.fieldInfo[f as never] delete newState.fieldMetaBase[f as never] }) return newState }) } /** * Pushes a value into an array field. */ pushFieldValue = >( field: TField, value: DeepValue extends any[] ? DeepValue[number] : never, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev) => [...(Array.isArray(prev) ? prev : []), value] as any, options, ) metaHelper(this).bumpArrayVersion(field) } insertFieldValue = async >( field: TField, index: number, value: DeepValue extends any[] ? DeepValue[number] : never, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev) => { return [ ...(prev as DeepValue[]).slice(0, index), value, ...(prev as DeepValue[]).slice(index), ] as any }, mergeOpts(options, { dontValidate: true }), ) const dontValidate = options?.dontValidate ?? false if (!dontValidate) { // Validate the whole array + all fields that have shifted await this.validateField(field, 'change') } // Shift down all meta after validating to make sure the new field has been mounted metaHelper(this).handleArrayInsert(field, index) if (!dontValidate) { await this.validateArrayFieldsStartingFrom(field, index, 'change') } } /** * Replaces a value into an array field at the specified index. */ replaceFieldValue = async >( field: TField, index: number, value: DeepValue extends any[] ? DeepValue[number] : never, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev) => { return (prev as DeepValue[]).map((d, i) => i === index ? value : d, ) as any }, mergeOpts(options, { dontValidate: true }), ) metaHelper(this).bumpArrayVersion(field) const dontValidate = options?.dontValidate ?? false if (!dontValidate) { // Validate the whole array + all fields that have shifted await this.validateField(field, 'change') await this.validateArrayFieldsStartingFrom(field, index, 'change') } } /** * Removes a value from an array field at the specified index. */ removeFieldValue = async >( field: TField, index: number, options?: UpdateMetaOptions, ) => { const fieldValue = this.getFieldValue(field) const lastIndex = Array.isArray(fieldValue) ? Math.max((fieldValue as Array).length - 1, 0) : null this.setFieldValue( field, (prev) => { return (prev as DeepValue[]).filter( (_d, i) => i !== index, ) as any }, mergeOpts(options, { dontValidate: true }), ) // Shift up all meta metaHelper(this).handleArrayRemove(field, index) if (lastIndex !== null) { const start = `${field}[${lastIndex}]` this.deleteField(start as never) } const dontValidate = options?.dontValidate ?? false if (!dontValidate) { // Validate the whole array + all fields that have shifted await this.validateField(field, 'change') await this.validateArrayFieldsStartingFrom(field, index, 'change') } } /** * Swaps the values at the specified indices within an array field. */ swapFieldValues = >( field: TField, index1: number, index2: number, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev: any) => { const prev1 = prev[index1]! const prev2 = prev[index2]! return setBy(setBy(prev, `${index1}`, prev2), `${index2}`, prev1) }, mergeOpts(options, { dontValidate: true }), ) // Swap meta metaHelper(this).handleArraySwap(field, index1, index2) const dontValidate = options?.dontValidate ?? false if (!dontValidate) { // Validate the whole array this.validateField(field, 'change') // Validate the swapped fields this.validateField(`${field}[${index1}]` as DeepKeys, 'change') this.validateField(`${field}[${index2}]` as DeepKeys, 'change') } } /** * Moves the value at the first specified index to the second specified index within an array field. */ moveFieldValues = >( field: TField, index1: number, index2: number, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev: any) => { const next: any = [...prev] next.splice(index2, 0, next.splice(index1, 1)[0]) return next }, mergeOpts(options, { dontValidate: true }), ) // Move meta between index1 and index2 metaHelper(this).handleArrayMove(field, index1, index2) const dontValidate = options?.dontValidate ?? false if (!dontValidate) { // Validate the whole array this.validateField(field, 'change') // Validate the moved fields this.validateField(`${field}[${index1}]` as DeepKeys, 'change') this.validateField(`${field}[${index2}]` as DeepKeys, 'change') } } /** * Clear all values within an array field. */ clearFieldValues = >( field: TField, options?: UpdateMetaOptions, ) => { const fieldValue = this.getFieldValue(field) const lastIndex = Array.isArray(fieldValue) ? Math.max((fieldValue as unknown[]).length - 1, 0) : null this.setFieldValue( field, [] as any, mergeOpts(options, { dontValidate: true }), ) metaHelper(this).bumpArrayVersion(field) if (lastIndex !== null) { for (let i = 0; i <= lastIndex; i++) { const fieldKey = `${field}[${i}]` this.deleteField(fieldKey as never) } } const dontValidate = options?.dontValidate ?? false if (!dontValidate) { // validate array change this.validateField(field, 'change') } } /** * Resets the field value and meta to default state */ resetField = >(field: TField) => { this.baseStore.setState((prev) => { const fieldDefault = this.getFieldInfo(field).instance?.options.defaultValue const formDefault = getBy(this.options.defaultValues, field) const targetValue = fieldDefault ?? formDefault return { ...prev, fieldMetaBase: { ...prev.fieldMetaBase, [field]: defaultFieldMeta, }, values: targetValue !== undefined ? setBy(prev.values, field, targetValue) : prev.values, } }) } /** * Updates the form's errorMap */ setErrorMap = ( errorMap: FormValidationErrorMap< TFormData, UnwrapFormValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormAsyncValidateOrFn >, ) => { batch(() => { Object.entries(errorMap).forEach(([key, value]) => { const errorMapKey = key as ValidationErrorMapKeys if (isGlobalFormValidationError(value)) { const { formError, fieldErrors } = normalizeError(value) for (const fieldName of Object.keys( this.fieldInfo, ) as DeepKeys[]) { const fieldMeta = this.getFieldMeta(fieldName) if (!fieldMeta) continue this.setFieldMeta(fieldName, (prev) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: fieldErrors?.[fieldName], }, errorSourceMap: { ...prev.errorSourceMap, [errorMapKey]: 'form', }, })) } this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: formError, }, })) } else { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: value, }, })) } }) }) } /** * Returns form and field level errors */ getAllErrors = (): { form: { errors: Array< NonNullable< | UnwrapFormValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormValidateOrFn | UnwrapFormAsyncValidateOrFn | UnwrapFormAsyncValidateOrFn > > errorMap: ValidationErrorMap< UnwrapFormValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormValidateOrFn, UnwrapFormAsyncValidateOrFn, UnwrapFormAsyncValidateOrFn > } fields: Record< DeepKeys, { errors: ValidationError[]; errorMap: ValidationErrorMap } > } => { return { form: { errors: this.state.errors, errorMap: this.state.errorMap, }, fields: Object.entries(this.state.fieldMeta).reduce( (acc, [fieldName, fieldMeta]) => { if ( Object.keys(fieldMeta as AnyFieldLikeMeta).length && (fieldMeta as AnyFieldLikeMeta).errors.length ) { acc[fieldName as DeepKeys] = { errors: (fieldMeta as AnyFieldLikeMeta).errors, errorMap: (fieldMeta as AnyFieldLikeMeta).errorMap, } } return acc }, {} as Record< DeepKeys, { errors: ValidationError[]; errorMap: ValidationErrorMap } >, ), } } /** * Parses the form's values with a given standard schema and returns * issues (if any). This method does NOT set any internal errors. * @param schema The standard schema to parse the form values with. */ parseValuesWithSchema = (schema: StandardSchemaV1) => { return standardSchemaValidators.validate( { value: this.state.values, validationSource: 'form' }, schema, ) } /** * Parses the form's values with a given standard schema and returns * issues (if any). This method does NOT set any internal errors. * @param schema The standard schema to parse the form values with. */ parseValuesWithSchemaAsync = ( schema: StandardSchemaV1, ) => { return standardSchemaValidators.validateAsync( { value: this.state.values, validationSource: 'form' }, schema, ) } } function normalizeError(rawError?: FormValidationError): { formError: ValidationError fieldErrors?: Partial, ValidationError>> } { if (rawError) { if (isGlobalFormValidationError(rawError)) { const formError = normalizeError(rawError.form).formError const fieldErrors = rawError.fields return { formError, fieldErrors } as never } return { formError: rawError } } return { formError: undefined } } function getErrorMapKey(cause: ValidationCause) { switch (cause) { case 'submit': return 'onSubmit' case 'blur': return 'onBlur' case 'mount': return 'onMount' case 'server': return 'onServer' case 'dynamic': return 'onDynamic' case 'change': default: return 'onChange' } }