import { batch, createStore } from '@tanstack/store' import { determineFieldLevelErrorSourceAndValue, determineFormLevelErrorSourceAndValue, evaluate, getAsyncValidatorArray, getSyncValidatorArray, isFieldInGroup, mergeOpts, } from './utils' import { defaultValidationLogic } from './ValidationLogic' import { isStandardSchemaValidator, standardSchemaValidators, } from './standardSchemaValidator' import { defaultFieldMeta } from './metaHelper' import { FieldApi } from './FieldApi' import { FieldLikeApiOptions } from './types' import type { ValidationLogicFn } from './ValidationLogic' import type { AnyFieldLikeMeta, AnyFieldLikeMetaBase, FieldErrorMapFromValidator, FieldInfo, FieldLikeAPI, FieldLikeMeta, FieldLikeMetaBase, FieldLikeOptions, FieldLikeState, FormLikeAPI, ListenerCause, UnwrapFieldAsyncValidateOrFn, UnwrapFieldValidateOrFn, UpdateMetaOptions, ValidationCause, ValidationError, ValidationErrorMap, } from './types' import type { FormApi, FormAsyncValidateOrFn, FormValidateOrFn, } from './FormApi' import type { AnyFieldApi } from './FieldApi' import type { StandardSchemaV1, TStandardSchemaValidatorValue, } from './standardSchemaValidator' import type { AsyncValidator, SyncValidator, Updater } from './utils' import type { ReadonlyStore } from '@tanstack/store' import type { DeepKeys, DeepKeysOfType, DeepValue, UnwrapOneLevelOfArray, } from './util-types' /** * @private */ export type FormGroupValidateFn< TParentData, TName extends DeepKeys, TData extends DeepValue = DeepValue, > = (props: { value: TData groupApi: FormGroupApi< TParentData, TName, TData, // 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 group validate function inner types, so it's not a big deal any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any > }) => unknown /** * @private */ export type FormGroupValidateOrFn< TParentData, TName extends DeepKeys, TData extends DeepValue = DeepValue, > = | FormGroupValidateFn | StandardSchemaV1 /** * @private */ export type FormGroupValidateAsyncFn< TParentData, TName extends DeepKeys, TData extends DeepValue = DeepValue, > = (options: { value: TData groupApi: FormGroupApi< TParentData, TName, TData, // 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 group validate function inner types, so it's not a big deal any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any > signal: AbortSignal }) => unknown | Promise /** * @private */ export type FormGroupAsyncValidateOrFn< TParentData, TName extends DeepKeys, TData extends DeepValue = DeepValue, > = | FormGroupValidateAsyncFn | StandardSchemaV1 /** * @private */ export type FormGroupListenerFn< TParentData, TName extends DeepKeys, TData extends DeepValue = DeepValue, > = (props: { value: TData groupApi: FormGroupApi< TParentData, TName, TData, // 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 group listener function inner types, so it's not a big deal any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any > }) => void // TODO: Add `listenTo` props back export interface FormGroupValidators< TParentData, TName extends DeepKeys, TData extends DeepValue, TOnMount extends undefined | FormGroupValidateOrFn, TOnChange extends | undefined | FormGroupValidateOrFn, TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, TOnBlur extends undefined | FormGroupValidateOrFn, TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, TOnSubmit extends | undefined | FormGroupValidateOrFn, TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, TOnDynamic extends | undefined | FormGroupValidateOrFn, TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, > { /** * An optional function, that runs on the mount event of input. */ onMount?: TOnMount /** * An optional function, that runs on the change event of input. * * @example z.string().min(1) */ onChange?: TOnChange /** * An optional property similar to `onChange` but async validation * * @example z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' }) */ onChangeAsync?: TOnChangeAsync /** * An optional number to represent how long the `onChangeAsync` should wait before running * * If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds */ onChangeAsyncDebounceMs?: number /** * An optional list of field names that should trigger this field's `onChange` and `onChangeAsync` events when its value changes */ // onChangeListenTo?: DeepKeys[] /** * An optional function, that runs on the blur event of input. * * @example z.string().min(1) */ onBlur?: TOnBlur /** * An optional property similar to `onBlur` but async validation. * * @example z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' }) */ onBlurAsync?: TOnBlurAsync /** * An optional number to represent how long the `onBlurAsync` should wait before running * * If set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds */ onBlurAsyncDebounceMs?: number /** * An optional list of field names that should trigger this field's `onBlur` and `onBlurAsync` events when its value changes */ // onBlurListenTo?: DeepKeys[] /** * An optional function, that runs on the submit event of form. * * @example z.string().min(1) */ onSubmit?: TOnSubmit /** * An optional property similar to `onSubmit` but async validation. * * @example z.string().refine(async (val) => val.length > 3, { message: 'Testing 123' }) */ onSubmitAsync?: TOnSubmitAsync onDynamic?: TOnDynamic onDynamicAsync?: TOnDynamicAsync onDynamicAsyncDebounceMs?: number } export interface FormGroupListeners< TParentData, TName extends DeepKeys, TData extends DeepValue = DeepValue, > { onChange?: FormGroupListenerFn onChangeDebounceMs?: number onBlur?: FormGroupListenerFn onBlurDebounceMs?: number onMount?: FormGroupListenerFn onUnmount?: FormGroupListenerFn onSubmit?: FormGroupListenerFn onGroupSubmit?: FormGroupListenerFn } interface FormGroupExtraOptions< in out TParentData, in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends | undefined | FormGroupValidateOrFn, in out TOnChange extends | undefined | FormGroupValidateOrFn, in out TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnBlur extends | undefined | FormGroupValidateOrFn, in out TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnSubmit extends | undefined | FormGroupValidateOrFn, in out TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnDynamic extends | undefined | FormGroupValidateOrFn, in out TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TSubmitMeta, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > { /** * A list of validators to pass to the field */ validators?: FormGroupValidators< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync > /** * 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 listeners which attach to the corresponding events */ listeners?: FormGroupListeners defaultState?: FormGroupState /** * Optional validation logic strategy to use for this group's own * validators (e.g. `revalidateLogic()`). When omitted, the parent form's * `validationLogic` (or the default) is used. */ validationLogic?: ValidationLogicFn /** * onSubmitMeta, the data passed from the handleSubmit handler, to the onSubmit function props */ onSubmitMeta?: 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` */ onGroupSubmit?: (props: { value: TData groupApi: FormGroupApi< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > meta: TSubmitMeta }) => any | Promise /** * Specify an action for scenarios where the user tries to submit an invalid form. */ onGroupSubmitInvalid?: (props: { value: TData groupApi: FormGroupApi< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > meta: TSubmitMeta }) => void } export interface FormGroupOptions< in out TParentData, in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends | undefined | FormGroupValidateOrFn, in out TOnChange extends | undefined | FormGroupValidateOrFn, in out TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnBlur extends | undefined | FormGroupValidateOrFn, in out TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnSubmit extends | undefined | FormGroupValidateOrFn, in out TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnDynamic extends | undefined | FormGroupValidateOrFn, in out TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TSubmitMeta, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > extends FieldLikeOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync >, FormGroupExtraOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > {} export interface FormGroupApiOptions< in out TParentData, in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends | undefined | FormGroupValidateOrFn, in out TOnChange extends | undefined | FormGroupValidateOrFn, in out TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnBlur extends | undefined | FormGroupValidateOrFn, in out TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnSubmit extends | undefined | FormGroupValidateOrFn, in out TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnDynamic extends | undefined | FormGroupValidateOrFn, in out TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TSubmitMeta, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > extends FormGroupOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > { form: FormApi< TParentData, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > } export interface FormGroupState { /** * 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 } function getDefaultFormGroupState( defaultState: Partial, ): FormGroupState { return { isSubmitted: defaultState.isSubmitted ?? false, isSubmitting: defaultState.isSubmitting ?? false, isValidating: defaultState.isValidating ?? false, submissionAttempts: defaultState.submissionAttempts ?? 0, isSubmitSuccessful: defaultState.isSubmitSuccessful ?? false, } } /** * @public * * A type representing the FormGroup API with all generics set to `any` for convenience. */ export type AnyFormGroupApi = FormGroupApi< any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any > /** * @public * * The `meta` shape exposed on `FormGroupApi.state.meta`. Mirrors * `FieldApi.state.meta` (since `FormGroupMeta extends FieldLikeMeta`) but * additionally surfaces the group's submission lifecycle and aggregated * validity flags. All derivation lives on the parent `FormApi` (in * `formGroupMetaDerived`), keeping per-instance `FormGroupApi.store` as * minimal as `FieldApi.store`. * * Aggregated booleans (`isTouched`, `isBlurred`, `isDirty`, `isPristine`, * `isDefaultValue`) are computed across the group's descendant fields * rather than the group's own field-meta entry. */ export interface FormGroupMeta< in out TParentData, in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends | undefined | FormGroupValidateOrFn, in out TOnChange extends | undefined | FormGroupValidateOrFn, in out TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnBlur extends | undefined | FormGroupValidateOrFn, in out TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnSubmit extends | undefined | FormGroupValidateOrFn, in out TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnDynamic extends | undefined | FormGroupValidateOrFn, in out TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends | undefined | FormAsyncValidateOrFn, > extends FieldLikeMeta< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync >, FormGroupState { isFieldsValidating: boolean isFieldsValid: boolean isGroupValid: boolean isValid: boolean canSubmit: boolean } /** * @public * * `FormGroupMeta` with all generics widened to `any`. */ export type AnyFormGroupMeta = FormGroupMeta< any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any > export interface FormGroupStoreState< in out TParentData, in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends | undefined | FormGroupValidateOrFn, in out TOnChange extends | undefined | FormGroupValidateOrFn, in out TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnBlur extends | undefined | FormGroupValidateOrFn, in out TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnSubmit extends | undefined | FormGroupValidateOrFn, in out TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnDynamic extends | undefined | FormGroupValidateOrFn, in out TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends | undefined | FormAsyncValidateOrFn, > { /** * The current value of the form group. */ value: TData /** * The current metadata of the form group, including aggregated validity, * group-level errors, and submission lifecycle. */ meta: FormGroupMeta< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync > } /** * @private * * Builds a default `FormGroupMeta` value, used as a fallback when the * parent form's `formGroupMetaDerived` store has no entry for this group * yet (e.g. between `new FormGroupApi(...)` and `mount()`). */ export function getDefaultFormGroupMeta( defaultMeta?: Partial, ): AnyFormGroupMeta { return { ...defaultFieldMeta, ...defaultMeta, errors: [], isPristine: true, isValid: true, isDefaultValue: true, isFieldsValidating: false, isFieldsValid: true, isGroupValid: true, canSubmit: true, isSubmitting: false, isSubmitted: false, isValidating: false, submissionAttempts: 0, isSubmitSuccessful: false, } as AnyFormGroupMeta } export class FormGroupApi< in out TParentData, in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends | undefined | FormGroupValidateOrFn, in out TOnChange extends | undefined | FormGroupValidateOrFn, in out TOnChangeAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnBlur extends | undefined | FormGroupValidateOrFn, in out TOnBlurAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnSubmit extends | undefined | FormGroupValidateOrFn, in out TOnSubmitAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TOnDynamic extends | undefined | FormGroupValidateOrFn, in out TOnDynamicAsync extends | undefined | FormGroupAsyncValidateOrFn, in out TSubmitMeta, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends | undefined | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > implements FormLikeAPI, FieldLikeAPI< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta, FormGroupExtraOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > > { /** * A reference to the form API instance. */ form: FormGroupApiOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta >['form'] /** * The field name. */ name: TName /** * The field options. */ options: FormGroupApiOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta > = {} as any /** * The field state store. */ store!: ReadonlyStore< FormGroupStoreState< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync > > /** * The current field state. */ get state() { return this.store.state } /** * @private * * Updates this group's submission lifecycle state on the parent form's * `baseStore` (where group state is now persisted), preserving entries * for any other mounted groups. After writing, the form's * `formGroupMetaDerived` re-derives so this group's `state.meta` picks * up the new lifecycle values automatically. */ private setFormGroupState = ( updater: (prev: FormGroupState) => FormGroupState, ) => { this.form.baseStore.setState((prev) => { const prevGroupState = prev.formGroupStateBase[this.name as never] ?? getDefaultFormGroupState({}) return { ...prev, formGroupStateBase: { ...prev.formGroupStateBase, [this.name as never]: updater(prevGroupState), }, } }) } timeoutIds: { validations: Record | null> listeners: Record | null> formListeners: Record | null> } /** * @private * * Tracks the set of fully-qualified child field names that this group's * validators last set form-source errors on, keyed by `errorMap` key. * Used to clear stale group-level field errors on subsequent runs without * trampling errors set by the parent form's validators. */ private _lastDistributedFieldNames: Partial>> = {} private fieldInfo: FieldInfo constructor( opts: FormGroupApiOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta >, ) { this.form = opts.form this.name = opts.name this.options = opts this.timeoutIds = { validations: {} as Record, listeners: {} as Record, formListeners: {} as Record, } this.fieldInfo = { instance: null, validationMetaMap: { onChange: undefined, onBlur: undefined, onSubmit: undefined, onMount: undefined, onServer: undefined, onDynamic: undefined, }, } this.store = createStore( ( prevVal: | FormGroupStoreState< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync > | undefined, ) => { // Subscribe to all form-level derived state that affects this // group. Mirrors `FieldApi.store`'s minimal pattern: per-instance // `store` only sources `{ value, meta }`; all heavy derivation // lives on the parent `FormApi` (in `formGroupMetaDerived`). this.form.formGroupMetaDerived.get() this.form.baseStore.get() const meta = (this.form.getFormGroupMeta(this.name as never) as | AnyFormGroupMeta | undefined) ?? getDefaultFormGroupMeta(opts.defaultMeta as never) let value = this.form.getFieldValue(this.name) if ( !meta.isTouched && (value as unknown) === undefined && this.options.defaultValue !== undefined && !evaluate(value, this.options.defaultValue) ) { value = this.options.defaultValue } if (prevVal && prevVal.value === value && prevVal.meta === meta) { return prevVal } return { value, meta, } as FormGroupStoreState< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync > }, ) this.handleSubmit = this.handleSubmit.bind(this) } /** * Updates the field instance with new options. */ update = ( opts: FormGroupApiOptions< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TSubmitMeta, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta >, ) => { this.options = opts this.name = opts.name // Default Value if (!this.state.meta.isTouched && this.options.defaultValue !== undefined) { const formField = this.form.getFieldValue(this.name) if (!evaluate(formField, opts.defaultValue)) { this.form.setFieldValue(this.name, opts.defaultValue as never, { dontUpdateMeta: true, dontValidate: true, dontRunListeners: true, }) } } if (!this.form.getFieldMeta(this.name)) { this.form.setFieldMeta(this.name, { ...defaultFieldMeta, ...(this.options.defaultMeta as Partial), } as never) } } /** * @private */ runValidator< TValue extends TStandardSchemaValidatorValue & { groupApi: AnyFormGroupApi }, TType extends 'validate' | 'validateAsync', >(props: { validate: TType extends 'validate' ? FormGroupValidateOrFn : FormGroupAsyncValidateOrFn value: TValue type: TType // When `api` is 'field', the return type cannot be `FormValidationError` }): unknown { if (isStandardSchemaValidator(props.validate)) { const result = standardSchemaValidators[props.type]( props.value, props.validate, ) as unknown // Standard schemas with `validationSource: 'form'` return `{ form, fields }`. // For groups we expose the same fan-out shape but under a `group` key // (a `form` key on a group-level validator would be misleading), so // remap the standard-schema result here. Manual functions on a group // are expected to return `{ group, fields }` already. if (props.type === 'validate') { return remapStandardSchemaResultForGroup(result) } return (result as Promise).then( remapStandardSchemaResultForGroup, ) } return (props.validate as FormGroupValidateFn)( props.value, ) as never } mount = () => { this.update(this.options as never) this.form.formGroupApis.add(this) this.fieldInfo.instance = this as never // Seed the parent form's `formGroupStateBase` entry for this group. // We always write so that `formGroupMetaDerived` re-derives now that // `formGroupApis` includes this instance — this is what makes // `state.meta` populated on the very first read after mount. Mirrors // `FieldApi.mount`'s lifecycle: per-group lifecycle state lives on // the form so it can be read off `FormApi` directly without walking // the mounted group instances. this.form.baseStore.setState((prev) => ({ ...prev, formGroupStateBase: { ...prev.formGroupStateBase, [this.name as never]: prev.formGroupStateBase[this.name as never] ?? getDefaultFormGroupState({ ...(this.options.defaultState as Partial), }), }, })) const { onMount } = this.options.validators || {} if (onMount) { const rawError = this.runValidator({ validate: onMount, value: { value: this.state.value, groupApi: this, validationSource: 'form', }, type: 'validate', }) let groupOwnRawError = rawError let groupFieldErrors: Record | undefined = undefined if (isGlobalGroupValidationError(rawError)) { groupOwnRawError = rawError.group groupFieldErrors = rawError.fields } const error = normalizeError(groupOwnRawError as ValidationError) if (error) { this.setMeta( (prev) => ({ ...prev, errorMap: { ...prev.errorMap, onMount: error, }, errorSourceMap: { ...prev.errorSourceMap, onMount: 'field', }, }) as never, ) } this.distributeFieldErrors('onMount', groupFieldErrors) } this.options.listeners?.onMount?.({ value: this.state.value, groupApi: this, }) return () => { // Stop any in-flight async validation or listener work tied to this instance. for (const [key, timeout] of Object.entries( this.timeoutIds.validations, )) { if (timeout) { clearTimeout(timeout) this.timeoutIds.validations[ key as keyof typeof this.timeoutIds.validations ] = null } } for (const [key, timeout] of Object.entries(this.timeoutIds.listeners)) { if (timeout) { clearTimeout(timeout) this.timeoutIds.listeners[ key as keyof typeof this.timeoutIds.listeners ] = null } } for (const [key, timeout] of Object.entries( this.timeoutIds.formListeners, )) { if (timeout) { clearTimeout(timeout) this.timeoutIds.formListeners[ key as keyof typeof this.timeoutIds.formListeners ] = null } } if (this.fieldInfo.instance !== this) return for (const [key, validationMeta] of Object.entries( this.fieldInfo.validationMetaMap, )) { validationMeta?.lastAbortController.abort() this.fieldInfo.validationMetaMap[ key as keyof typeof this.fieldInfo.validationMetaMap ] = undefined } this.form.formGroupApis.delete(this) // Reset this group's submission lifecycle state on the form. Mirrors // `FieldApi.mount`'s teardown which resets `fieldMetaBase` for the // unmounting field while preserving the entry on the parent store. this.form.baseStore.setState((prev) => ({ ...prev, formGroupStateBase: { ...prev.formGroupStateBase, [this.name as never]: getDefaultFormGroupState({}), }, })) this.fieldInfo.instance = null this.options.listeners?.onUnmount?.({ value: this.state.value, groupApi: this, }) } } /** * Sets the field value and run the `change` validator. */ setValue = (updater: Updater, options?: UpdateMetaOptions) => { this.form.setFieldValue( this.name, updater as never, mergeOpts(options, { dontRunListeners: true, dontValidate: true }), ) if (!options?.dontRunListeners) { this.triggerOnChangeListener() } if (!options?.dontValidate) { this.validate('change') } } getMeta = () => this.store.state.meta /** * Sets the field metadata. */ setMeta = ( updater: Updater< FieldLikeMetaBase< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TFormOnMount, TFormOnChange, TFormOnChangeAsync, TFormOnBlur, TFormOnBlurAsync, TFormOnSubmit, TFormOnSubmitAsync, TFormOnDynamic, TFormOnDynamicAsync > >, ) => this.form.setFieldMeta(this.name, updater) /** * Gets the field information object. */ getInfo = () => this.fieldInfo /** * @private */ getRelatedFields = () => { const fields = Object.values(this.form.fieldInfo) as FieldInfo[] const relatedFields: AnyFieldApi[] = [] for (const field of fields) { if (!field.instance) continue // TODO: How to handle FormGroups? if (!(field.instance instanceof FieldApi)) continue if (field.instance.name.startsWith(this.name)) { relatedFields.push(field.instance) } } return relatedFields } /** * @private */ getRelatedFieldMetasDerived = () => { const fields = Object.entries(this.form.fieldMetaDerived.state) as [ string, AnyFieldLikeMeta, ][] const relatedFieldMetas: (AnyFieldLikeMeta & { name: string })[] = [] for (const [fieldName, fieldMeta] of fields) { // Skip the group's own self-entry — its validity is tracked via // `isGroupValid`. Including it here would conflate group-level // validation with field-level validation in `isFieldsValid` etc. if (fieldName === this.name) continue if (isFieldInGroup(this.name, fieldName)) { relatedFieldMetas.push({ ...fieldMeta, name: fieldName }) } } return relatedFieldMetas } /** * @private * * Builds a fully-qualified field name from a path that is relative to this * group, supporting both dot (`name`, `nested.value`) and bracket * (`[0].name`) notation. */ private buildChildFieldName = (relativeName: string): string => { if (relativeName === '') return this.name as string if (relativeName.startsWith('[')) return `${this.name}${relativeName}` return `${this.name}.${relativeName}` } /** * @private * * Distributes a `{ fields: { ... } }` payload returned by one of this * group's own validators onto the corresponding child fields. Tracks * which fields have been touched so subsequent runs can clear stale * errors without trampling errors set by the parent form's validators. */ private distributeFieldErrors = ( errorMapKey: string, fieldErrors: Record | undefined, ): boolean => { const previousNames = this._lastDistributedFieldNames[errorMapKey] ?? new Set() const currentNames = new Set() if (fieldErrors) { for (const [relativeName, err] of Object.entries(fieldErrors)) { if (err === undefined || err === null || err === false) continue currentNames.add(this.buildChildFieldName(relativeName)) } } const allNames = new Set([...previousNames, ...currentNames]) let hasErrored = false for (const fullName of allNames) { const relativeName = fullName.startsWith(this.name + '[') ? fullName.slice((this.name as string).length) : fullName.slice((this.name as string).length + 1) const newFormValidatorError = fieldErrors?.[relativeName] as | ValidationError | undefined const fieldMeta = this.form.getFieldMeta(fullName as never) if (!fieldMeta && !newFormValidatorError) continue const previousErrorValue = fieldMeta?.errorMap[errorMapKey as never] as | ValidationError | undefined const isPreviousErrorFromFormValidator = (fieldMeta?.errorSourceMap[errorMapKey as never] as | string | undefined) === 'form' const { newErrorValue, newSource } = determineFormLevelErrorSourceAndValue({ newFormValidatorError, isPreviousErrorFromFormValidator, previousErrorValue, }) if (newErrorValue) hasErrored = true if ( previousErrorValue === newErrorValue && fieldMeta?.errorSourceMap[errorMapKey as never] === newSource ) { continue } this.form.setFieldMeta(fullName as never, (prev = defaultFieldMeta) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { ...prev.errorSourceMap, [errorMapKey]: newSource, }, })) } this._lastDistributedFieldNames[errorMapKey] = currentNames return hasErrored } /** * @private */ validateSync = ( cause: ValidationCause, errorFromForm: ValidationErrorMap, opts: { skipRelatedFieldValidation?: boolean } = {}, ) => { const validates = getSyncValidatorArray(cause, { ...this.options, form: this.form, group: this, validationLogic: this.options.validationLogic || this.form.options.validationLogic || defaultValidationLogic, }) const relatedFields = opts.skipRelatedFieldValidation ? [] : this.getRelatedFields() const relatedFieldValidates = relatedFields.reduce( (acc, field) => { const fieldValidates = getSyncValidatorArray(cause, { ...field.options, form: field.form, validationLogic: field.form.options.validationLogic || defaultValidationLogic, }) fieldValidates.forEach((validate) => { ;(validate as any).field = field }) return acc.concat(fieldValidates as never) }, [] as Array< SyncValidator & { field: AnyFieldApi } >, ) // Needs type cast as eslint errantly believes this is always falsy let hasErrored = false as boolean batch(() => { const validateFieldOrGroupFn = ( fieldOrGroup: AnyFieldApi | AnyFormGroupApi, validateObj: SyncValidator, ) => { const errorMapKey = getErrorMapKey(validateObj.cause) const isGroup = fieldOrGroup === this let rawError: unknown = undefined if (validateObj.validate) { rawError = (fieldOrGroup as any).runValidator({ validate: validateObj.validate, value: { value: fieldOrGroup.store.state.value, // For the group's own validators we want standard schemas to // produce a `{ form, fields }` shape (with relative keys) so // we can fan errors out to children. Field-level validators on // related fields keep the regular field source. validationSource: isGroup ? 'form' : 'field', ...(fieldOrGroup instanceof FormGroupApi ? { groupApi: fieldOrGroup, } : { fieldApi: fieldOrGroup }), } as never, type: 'validate', }) } let groupOwnRawError: unknown = rawError let groupFieldErrors: Record | undefined = undefined if (isGroup && isGlobalGroupValidationError(rawError)) { groupOwnRawError = rawError.group groupFieldErrors = rawError.fields } const fieldLevelError = normalizeError( groupOwnRawError as ValidationError, ) const formLevelError = errorFromForm[errorMapKey] const { newErrorValue, newSource } = determineFieldLevelErrorSourceAndValue({ formLevelError, fieldLevelError, }) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (fieldOrGroup.state.meta.errorMap?.[errorMapKey] !== newErrorValue) { fieldOrGroup.setMeta((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { ...prev.errorSourceMap, [errorMapKey]: newSource, }, })) } if (newErrorValue) { hasErrored = true } if (isGroup) { const distributedHasErrored = this.distributeFieldErrors( errorMapKey, groupFieldErrors, ) if (distributedHasErrored) { hasErrored = true } } } for (const validateObj of validates) { validateFieldOrGroupFn(this, validateObj) } for (const fieldValidateObj of relatedFieldValidates) { if (!fieldValidateObj.validate) continue validateFieldOrGroupFn(fieldValidateObj.field, fieldValidateObj) } }) /** * 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.meta.errorMap?.[submitErrKey] && cause !== 'submit' && !hasErrored ) { this.setMeta((prev) => ({ ...prev, errorMap: { ...prev.errorMap, [submitErrKey]: undefined, }, errorSourceMap: { ...prev.errorSourceMap, [submitErrKey]: undefined, }, })) } return { hasErrored } } /** * @private */ validateAsync = async ( cause: ValidationCause, formValidationResultPromise: Promise< FieldErrorMapFromValidator< TParentData, TName, TData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync > >, opts: { skipRelatedFieldValidation?: boolean } = {}, ) => { const validates = getAsyncValidatorArray(cause, { ...this.options, form: this.form, group: this, validationLogic: this.options.validationLogic || this.form.options.validationLogic || defaultValidationLogic, }) // Get the field-specific error messages that are coming from the form's validator const asyncFormValidationResults = await formValidationResultPromise const relatedFields = opts.skipRelatedFieldValidation ? [] : this.getRelatedFields() const relatedFieldValidates = relatedFields.reduce( (acc, field) => { const fieldValidates = getAsyncValidatorArray(cause, { ...field.options, form: field.form, validationLogic: field.form.options.validationLogic || defaultValidationLogic, }) fieldValidates.forEach((validate) => { ;(validate as any).field = field }) return acc.concat(fieldValidates as never) }, [] as Array< AsyncValidator & { field: AnyFieldApi } >, ) /** * 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 validatesPromises: Promise[] = [] const linkedPromises: Promise[] = [] // Check if there are actual async validators to run before setting isValidating // This prevents unnecessary re-renders when there are no async validators // See: https://github.com/TanStack/form/issues/1130 const hasAsyncValidators = validates.some((v) => v.validate) || relatedFieldValidates.some((v) => v.validate) if (hasAsyncValidators) { if (!this.state.meta.isValidating) { this.setMeta((prev) => ({ ...prev, isValidating: true })) } for (const linkedField of relatedFields) { linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) } } const validateFieldOrGroupAsyncFn = ( fieldOrGroup: AnyFieldApi | AnyFormGroupApi, validateObj: AsyncValidator, promises: Promise[], ) => { const errorMapKey = getErrorMapKey(validateObj.cause) const fieldInfo = fieldOrGroup.getInfo() const fieldValidatorMeta = fieldInfo.validationMetaMap[errorMapKey] fieldValidatorMeta?.lastAbortController.abort() const controller = new AbortController() fieldInfo.validationMetaMap[errorMapKey] = { lastAbortController: controller, } const isGroup = fieldOrGroup === this promises.push( new Promise(async (resolve) => { let rawError!: ValidationError | undefined try { rawError = await new Promise((rawResolve, rawReject) => { if (fieldOrGroup.timeoutIds.validations[validateObj.cause]) { clearTimeout( fieldOrGroup.timeoutIds.validations[validateObj.cause]!, ) } fieldOrGroup.timeoutIds.validations[validateObj.cause] = setTimeout(async () => { if (controller.signal.aborted) return rawResolve(undefined) try { rawResolve( await this.runValidator({ validate: validateObj.validate, value: { value: fieldOrGroup.store.state.value, signal: controller.signal, // See sync counterpart: produce `{ form, fields }` // from standard schemas attached to the group so we // can fan errors out to children. validationSource: isGroup ? 'form' : 'field', ...(fieldOrGroup instanceof FormGroupApi ? { groupApi: fieldOrGroup, } : { fieldApi: fieldOrGroup }), } as never, type: 'validateAsync', }), ) } catch (e) { rawReject(e) } }, validateObj.debounceMs) }) } catch (e: unknown) { rawError = e as ValidationError } if (controller.signal.aborted) return resolve(undefined) let groupOwnRawError: ValidationError | undefined = rawError let groupFieldErrors: Record | undefined = undefined if (isGroup && isGlobalGroupValidationError(rawError)) { groupOwnRawError = rawError.group as ValidationError | undefined groupFieldErrors = rawError.fields } const fieldLevelError = normalizeError(groupOwnRawError) const formLevelError = asyncFormValidationResults[ fieldOrGroup.name as keyof typeof asyncFormValidationResults ]?.[errorMapKey] const { newErrorValue, newSource } = determineFieldLevelErrorSourceAndValue({ formLevelError, fieldLevelError, }) if (fieldOrGroup.getInfo().instance !== fieldOrGroup) { return resolve(undefined) } fieldOrGroup.setMeta((prev) => { return { ...prev, errorMap: { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition ...prev?.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { ...prev.errorSourceMap, [errorMapKey]: newSource, }, } }) if (isGroup) { this.distributeFieldErrors(errorMapKey, groupFieldErrors) } resolve(newErrorValue) }), ) } // TODO: Dedupe this logic to reduce bundle size for (const validateObj of validates) { if (!validateObj.validate) continue validateFieldOrGroupAsyncFn(this, validateObj, validatesPromises) } for (const fieldValitateObj of relatedFieldValidates) { if (!fieldValitateObj.validate) continue validateFieldOrGroupAsyncFn( fieldValitateObj.field, fieldValitateObj, linkedPromises, ) } let results: ValidationError[] = [] if (validatesPromises.length || linkedPromises.length) { results = await Promise.all(validatesPromises) await Promise.all(linkedPromises) } // Only reset isValidating if we set it to true earlier if (hasAsyncValidators) { this.setMeta((prev) => ({ ...prev, isValidating: false })) for (const linkedField of relatedFields) { linkedField.setMeta((prev) => ({ ...prev, isValidating: false })) } } return results.filter(Boolean) } /** * 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.getRelatedFields()).forEach((fieldInstance) => { // 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 (!fieldInstance.store.state.meta.isTouched) { // Mark them as touched fieldInstance.setMeta((prev) => ({ ...prev, isTouched: true })) } }) }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() } validateArrayFieldsStartingFrom = < TField extends DeepKeysOfType, >( field: TField, index: number, cause: ValidationCause, ) => { return this.form.validateArrayFieldsStartingFrom(field, index, cause) } validateField = >( field: TField, cause: ValidationCause, ) => { return this.form.validateField(field, cause) } getFieldValue = >( field: TField, ) => { return this.form.getFieldValue(field) } getFieldMeta = >( field: TField, ) => { return this.form.getFieldMeta(field) } setFieldMeta = >( field: TField, updater: Updater, ) => { return this.form.setFieldMeta(field, updater) } setFieldValue = >( field: TField, value: any, ) => { return this.form.setFieldValue(field, value) } deleteField = >( field: TField, ) => { return this.form.deleteField(field) } pushFieldValue = >( field: TField, value: any, ) => { return this.form.pushFieldValue(field, value) } insertFieldValue = >( field: TField, index: number, value: any, ) => { return this.form.insertFieldValue(field, index, value) } replaceFieldValue = >( field: TField, index: number, value: any, ) => { return this.form.replaceFieldValue(field, index, value) } swapFieldValues = >( field: TField, index1: number, index2: number, ) => { return this.form.swapFieldValues(field, index1, index2) } moveFieldValues = >( field: TField, fromIndex: number, toIndex: number, ) => { return this.form.moveFieldValues(field, fromIndex, toIndex) } clearFieldValues = >( field: TField, ) => { return this.form.clearFieldValues(field) } resetField = >( field: TField, ) => { return this.form.resetField(field) } removeFieldValue = >( field: TField, index: number, ) => { return this.form.removeFieldValue(field, index) } areRelatedFieldsValid = () => { return Object.values(this.getRelatedFields()).every( (field) => field.state.meta.isValid, ) } /** * Validates the form group and all related children. */ validate = ( cause: ValidationCause, opts?: { skipFormValidation?: boolean skipRelatedFieldValidation?: boolean }, ): ValidationError[] | Promise => { // Attempt to sync validate first const { fieldsErrorMap } = opts?.skipFormValidation ? { fieldsErrorMap: {} as never } : this.form.validateSync(cause, { dontUpdateFormErrorMap: true, filterFieldNames: (fieldName) => isFieldInGroup(this.name, fieldName), }) const { hasErrored } = this.validateSync( cause, fieldsErrorMap[this.name] ?? {}, { skipRelatedFieldValidation: opts?.skipRelatedFieldValidation }, ) if (hasErrored && !this.options.asyncAlways) { this.getInfo().validationMetaMap[ getErrorMapKey(cause) ]?.lastAbortController.abort() return this.state.meta.errors } // No error? Attempt async validation const formValidationResultPromise = opts?.skipFormValidation ? Promise.resolve({}) : this.form.validateAsync(cause, { dontUpdateFormErrorMap: true, filterFieldNames: (fieldName) => isFieldInGroup(this.name, fieldName), }) return this.validateAsync(cause, formValidationResultPromise, { skipRelatedFieldValidation: opts?.skipRelatedFieldValidation, }) } /** * @private */ triggerOnChangeListener = () => { const formDebounceMs = this.form.options.listeners?.onChangeGroupDebounceMs if (formDebounceMs && formDebounceMs > 0) { if (this.timeoutIds.formListeners.change) { clearTimeout(this.timeoutIds.formListeners.change) } this.timeoutIds.formListeners.change = setTimeout(() => { this.form.options.listeners?.onChangeGroup?.({ formApi: this.form, groupApi: this, }) }, formDebounceMs) } else { this.form.options.listeners?.onChangeGroup?.({ formApi: this.form, groupApi: this, }) } const fieldDebounceMs = this.options.listeners?.onChangeDebounceMs if (fieldDebounceMs && fieldDebounceMs > 0) { if (this.timeoutIds.listeners.change) { clearTimeout(this.timeoutIds.listeners.change) } this.timeoutIds.listeners.change = setTimeout(() => { this.options.listeners?.onChange?.({ value: this.state.value, groupApi: this, }) }, fieldDebounceMs) } else { this.options.listeners?.onChange?.({ value: this.state.value, groupApi: this, }) } } /** * @private */ triggerOnSubmitListener = () => { this.options.listeners?.onSubmit?.({ value: this.state.value, groupApi: this, }) } // 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.setFormGroupState((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.getRelatedFields()).forEach((field) => { // If any fields are not touched if (!field.state.meta.isTouched) { // Mark them as touched field.setMeta((prev) => ({ ...prev, isTouched: true })) } }) }) const submitMetaArg = submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta) this.setFormGroupState((d) => ({ ...d, isSubmitting: true })) const done = () => { this.setFormGroupState((prev) => ({ ...prev, isSubmitting: false })) } await this.validateAllFields('submit') // Fields are invalid, do not submit if (!this.areRelatedFieldsValid()) { done() this.options.onGroupSubmitInvalid?.({ value: this.state.value, groupApi: this, meta: submitMetaArg, }) return } await this.validate('submit', { // This has already happened in the previous step skipRelatedFieldValidation: true, }) // Group (or related fields) is invalid, do not submit. Mirrors // `FormApi._handleSubmit`'s check against the derived `state.isValid`, // which includes both the group's own validators and any form-level // errors propagated onto related fields by `validate('submit')` above // (e.g. `onDynamic` errors via `revalidateLogic`). if (!this.areRelatedFieldsValid() || !this.state.meta.isValid) { done() this.options.onGroupSubmitInvalid?.({ value: this.state.value, groupApi: this, meta: submitMetaArg, }) return } batch(() => { void Object.values(this.getRelatedFields()).forEach((field) => { field.options.listeners?.onGroupSubmit?.({ value: field.state.value, fieldApi: field, }) }) }) this.options.listeners?.onSubmit?.({ groupApi: this, value: this.state.value, }) try { // Run the submit code await this.options.onGroupSubmit?.({ value: this.state.value, groupApi: this, meta: submitMetaArg, }) batch(() => { this.setFormGroupState((prev) => ({ ...prev, isSubmitted: true, isSubmitSuccessful: true, // Set isSubmitSuccessful to true on successful submission })) done() }) } catch (err) { this.setFormGroupState((prev) => ({ ...prev, isSubmitSuccessful: false, // Ensure isSubmitSuccessful is false if an error occurs })) done() throw err } } } function normalizeError(rawError?: ValidationError) { if (rawError) { return rawError } return undefined } /** * @private * * Type guard for the group-level analogue of `GlobalFormValidationError`. * Group-level validators that want to fan errors out to child fields return * `{ group?: ValidationError, fields: { ...relativePath: ValidationError } }`. */ function isGlobalGroupValidationError( error: unknown, ): error is { group?: unknown; fields?: Record } { return !!error && typeof error === 'object' && 'fields' in error } /** * @private * * Standard schemas produce `{ form, fields }`. For groups we prefer to expose * a `{ group, fields }` shape because `form` would be misleading on a * group-level validator. This rename keeps the rest of the group validation * pipeline operating on a single shape. */ function remapStandardSchemaResultForGroup(result: unknown): unknown { if (!result || typeof result !== 'object') return result if (!('form' in result) && !('fields' in result)) return result const { form, fields, ...rest } = result as { form?: unknown fields?: unknown } return { ...rest, group: form, fields } } 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' } }