# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- docs/installation.md ---
---
id: installation
title: Installation
---

TanStack Form is compatible with various front-end frameworks, including React, Vue, and Solid. To use TanStack Form with your desired framework, install the corresponding adapter via your preferred package manager:

### React Example

```bash
# npm
$ npm i @tanstack/react-form
# pnpm
$ pnpm add @tanstack/react-form
# bun
$ bun add @tanstack/react-form
# yarn
$ yarn add @tanstack/react-form
```

### Vue Example

```bash
# npm
$ npm i @tanstack/vue-form
# pnpm
$ pnpm add @tanstack/vue-form
# bun
$ bun add @tanstack/vue-form
# yarn
$ yarn add @tanstack/vue-form
```

### Angular Example

```bash
# npm
$ npm i @tanstack/angular-form
# pnpm
$ pnpm add @tanstack/angular-form
# bun
$ bun add @tanstack/angular-form
# yarn
$ yarn add @tanstack/angular-form
```

### Solid Example

```bash
# npm
$ npm i @tanstack/solid-form
# pnpm
$ pnpm add @tanstack/solid-form
# bun
$ bun add @tanstack/solid-form
# yarn
$ yarn add @tanstack/solid-form
```

### Lit Example

```bash
# npm
$ npm i @tanstack/lit-form
# pnpm
$ pnpm add @tanstack/lit-form
# bun
$ bun add @tanstack/lit-form
# yarn
$ yarn add @tanstack/lit-form
```

### Svelte Example

```bash
# npm
$ npm i @tanstack/svelte-form
# pnpm
$ pnpm add @tanstack/svelte-form
# bun
$ bun add @tanstack/svelte-form
# yarn
$ yarn add @tanstack/svelte-form
```

> Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from `node_modules` yourselves.


--- docs/overview.md ---
---
id: overview
title: Overview
---

TanStack Form is the ultimate solution for handling forms in web applications, providing a powerful and flexible approach to form management. Designed with first-class TypeScript support, headless UI components, and a framework-agnostic design, it streamlines form handling and ensures a seamless experience across various front-end frameworks.

## Motivation

Most web frameworks do not offer a comprehensive solution for form handling, leaving developers to create their own custom implementations or rely on less-capable libraries. This often results in a lack of consistency, poor performance, and increased development time. TanStack Form aims to address these challenges by providing an all-in-one solution for managing forms that is both powerful and easy to use.

With TanStack Form, developers can tackle common form-related challenges such as:

- Reactive data binding and state management
- Complex validation and error handling
- Accessibility and responsive design
- Internationalization and localization
- Cross-platform compatibility and custom styling

By providing a complete solution for these challenges, TanStack Form empowers developers to build robust and user-friendly forms with ease.

## Enough talk, show me some code already!

In the example below, you can see TanStack Form in action with the React framework adapter:

[Open in CodeSandbox](https://codesandbox.io/s/github/tanstack/form/tree/main/examples/react/simple)

```tsx
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { useForm } from '@tanstack/react-form'
import type { AnyFieldApi } from '@tanstack/react-form'

function FieldInfo({ field }: { field: AnyFieldApi }) {
  return (
    <>
      {field.state.meta.isTouched && !field.state.meta.isValid ? (
        <em>{field.state.meta.errors.join(', ')}</em>
      ) : null}
      {field.state.meta.isValidating ? 'Validating...' : null}
    </>
  )
}

export default function App() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    onSubmit: async ({ value }) => {
      // Do something with form data
      console.log(value)
    },
  })

  return (
    <div>
      <h1>Simple Form Example</h1>
      <form
        onSubmit={(e) => {
          e.preventDefault()
          e.stopPropagation()
          form.handleSubmit()
        }}
      >
        <div>
          {/* A type-safe field component*/}
          <form.Field
            name="firstName"
            validators={{
              onChange: ({ value }) =>
                !value
                  ? 'A first name is required'
                  : value.length < 3
                    ? 'First name must be at least 3 characters'
                    : undefined,
              onChangeAsyncDebounceMs: 500,
              onChangeAsync: async ({ value }) => {
                await new Promise((resolve) => setTimeout(resolve, 1000))
                return (
                  value.includes('error') && 'No "error" allowed in first name'
                )
              },
            }}
            children={(field) => {
              // Avoid hasty abstractions. Render props are great!
              return (
                <>
                  <label htmlFor={field.name}>First Name:</label>
                  <input
                    id={field.name}
                    name={field.name}
                    value={field.state.value}
                    onBlur={field.handleBlur}
                    onChange={(e) => field.handleChange(e.target.value)}
                  />
                  <FieldInfo field={field} />
                </>
              )
            }}
          />
        </div>
        <div>
          <form.Field
            name="lastName"
            children={(field) => (
              <>
                <label htmlFor={field.name}>Last Name:</label>
                <input
                  id={field.name}
                  name={field.name}
                  value={field.state.value}
                  onBlur={field.handleBlur}
                  onChange={(e) => field.handleChange(e.target.value)}
                />
                <FieldInfo field={field} />
              </>
            )}
          />
        </div>
        <form.Subscribe
          selector={(state) => [state.canSubmit, state.isSubmitting]}
          children={([canSubmit, isSubmitting]) => (
            <button type="submit" disabled={!canSubmit}>
              {isSubmitting ? '...' : 'Submit'}
            </button>
          )}
        />
      </form>
    </div>
  )
}

const rootElement = document.getElementById('root')!

createRoot(rootElement).render(<App />)
```

## You talked me into it, so what now?

- Learn TanStack Form at your own pace with our thorough [Walkthrough Guide](./installation) and [API Reference](./reference/classes/FormApi)


## Links discovered
- [Open in CodeSandbox](https://codesandbox.io/s/github/tanstack/form/tree/main/examples/react/simple)
- [Walkthrough Guide](https://github.com/tanstack/form/blob/main/docs/installation.md)
- [API Reference](https://github.com/tanstack/form/blob/main/docs/reference/classes/FormApi.md)

--- docs/reference/index.md ---
---
id: "@tanstack/form-core"
title: "@tanstack/form-core"
---

# @tanstack/form-core

## Classes

- [FieldApi](classes/FieldApi.md)
- [FieldGroupApi](classes/FieldGroupApi.md)
- [FormApi](classes/FormApi.md)

## Interfaces

- [AnyDeepKeyAndValue](interfaces/AnyDeepKeyAndValue.md)
- [ArrayDeepKeyAndValue](interfaces/ArrayDeepKeyAndValue.md)
- [BaseFormOptions](interfaces/BaseFormOptions.md)
- [FieldApiOptions](interfaces/FieldApiOptions.md)
- [FieldGroupOptions](interfaces/FieldGroupOptions.md)
- [FieldGroupState](interfaces/FieldGroupState.md)
- [FieldListeners](interfaces/FieldListeners.md)
- [FieldOptions](interfaces/FieldOptions.md)
- [FieldValidators](interfaces/FieldValidators.md)
- [FormListeners](interfaces/FormListeners.md)
- [FormOptions](interfaces/FormOptions.md)
- [FormState](interfaces/FormState.md)
- [FormValidators](interfaces/FormValidators.md)
- [ObjectDeepKeyAndValue](interfaces/ObjectDeepKeyAndValue.md)
- [StandardSchemaV1Issue](interfaces/StandardSchemaV1Issue.md)
- [TupleDeepKeyAndValue](interfaces/TupleDeepKeyAndValue.md)
- [UnknownDeepKeyAndValue](interfaces/UnknownDeepKeyAndValue.md)
- [ValidationLogicProps](interfaces/ValidationLogicProps.md)

## Type Aliases

- [AllObjectKeys](type-aliases/AllObjectKeys.md)
- [AllTupleKeys](type-aliases/AllTupleKeys.md)
- [AnyFieldApi](type-aliases/AnyFieldApi.md)
- [AnyFieldGroupApi](type-aliases/AnyFieldGroupApi.md)
- [AnyFieldMeta](type-aliases/AnyFieldMeta.md)
- [AnyFieldMetaBase](type-aliases/AnyFieldMetaBase.md)
- [AnyFieldMetaDerived](type-aliases/AnyFieldMetaDerived.md)
- [AnyFormApi](type-aliases/AnyFormApi.md)
- [AnyFormOptions](type-aliases/AnyFormOptions.md)
- [AnyFormState](type-aliases/AnyFormState.md)
- [ArrayAccessor](type-aliases/ArrayAccessor.md)
- [BaseFormState](type-aliases/BaseFormState.md)
- [BroadcastFormApi](type-aliases/BroadcastFormApi.md)
- [BroadcastFormId](type-aliases/BroadcastFormId.md)
- [BroadcastFormState](type-aliases/BroadcastFormState.md)
- [BroadcastFormSubmissionState](type-aliases/BroadcastFormSubmissionState.md)
- [DeepKeyAndValueArray](type-aliases/DeepKeyAndValueArray.md)
- [DeepKeyAndValueObject](type-aliases/DeepKeyAndValueObject.md)
- [DeepKeyAndValueTuple](type-aliases/DeepKeyAndValueTuple.md)
- [DeepKeys](type-aliases/DeepKeys.md)
- [DeepKeysAndValues](type-aliases/DeepKeysAndValues.md)
- [DeepKeysAndValuesImpl](type-aliases/DeepKeysAndValuesImpl.md)
- [DeepKeysOfType](type-aliases/DeepKeysOfType.md)
- [DeepRecord](type-aliases/DeepRecord.md)
- [DeepValue](type-aliases/DeepValue.md)
- [DerivedFormState](type-aliases/DerivedFormState.md)
- [EventClientEventMap](type-aliases/EventClientEventMap.md)
- [EventClientEventNames](type-aliases/EventClientEventNames.md)
- [ExtractGlobalFormError](type-aliases/ExtractGlobalFormError.md)
- [FieldInfo](type-aliases/FieldInfo.md)
- [FieldMeta](type-aliases/FieldMeta.md)
- [FieldMetaBase](type-aliases/FieldMetaBase.md)
- [FieldMetaDerived](type-aliases/FieldMetaDerived.md)
- [FieldsMap](type-aliases/FieldsMap.md)
- [FieldState](type-aliases/FieldState.md)
- [FormValidateFn](type-aliases/FormValidateFn.md)
- [FormValidationError](type-aliases/FormValidationError.md)
- [FormValidator](type-aliases/FormValidator.md)
- [Nullable](type-aliases/Nullable.md)
- [ObjectAccessor](type-aliases/ObjectAccessor.md)
- [ObjectValue](type-aliases/ObjectValue.md)
- [StandardSchemaV1](type-aliases/StandardSchemaV1.md)
- [TStandardSchemaValidatorIssue](type-aliases/TStandardSchemaValidatorIssue.md)
- [TStandardSchemaValidatorValue](type-aliases/TStandardSchemaValidatorValue.md)
- [TupleAccessor](type-aliases/TupleAccessor.md)
- [UnknownAccessor](type-aliases/UnknownAccessor.md)
- [UnwrapFieldAsyncValidateOrFn](type-aliases/UnwrapFieldAsyncValidateOrFn.md)
- [UnwrapFieldValidateOrFn](type-aliases/UnwrapFieldValidateOrFn.md)
- [UnwrapFormAsyncValidateOrFn](type-aliases/UnwrapFormAsyncValidateOrFn.md)
- [UnwrapFormValidateOrFn](type-aliases/UnwrapFormValidateOrFn.md)
- [Updater](type-aliases/Updater.md)
- [UpdaterFn](type-aliases/UpdaterFn.md)
- [ValidationError](type-aliases/ValidationError.md)
- [ValidationLogicFn](type-aliases/ValidationLogicFn.md)
- [ValidationMeta](type-aliases/ValidationMeta.md)
- [ValidationSource](type-aliases/ValidationSource.md)

## Variables

- [defaultValidationLogic](variables/defaultValidationLogic.md)
- [formEventClient](variables/formEventClient.md)
- [standardSchemaValidators](variables/standardSchemaValidators.md)
- [throttleFormState](variables/throttleFormState.md)

## Functions

- [createFieldMap](functions/createFieldMap.md)
- [evaluate](functions/evaluate.md)
- [formOptions](functions/formOptions.md)
- [isGlobalFormValidationError](functions/isGlobalFormValidationError.md)
- [isStandardSchemaValidator](functions/isStandardSchemaValidator.md)
- [mergeForm](functions/mergeForm.md)
- [revalidateLogic](functions/revalidateLogic.md)
- [uuid](functions/uuid.md)


## Links discovered
- [FieldApi](https://github.com/tanstack/form/blob/main/docs/reference/classes/FieldApi.md)
- [FieldGroupApi](https://github.com/tanstack/form/blob/main/docs/reference/classes/FieldGroupApi.md)
- [FormApi](https://github.com/tanstack/form/blob/main/docs/reference/classes/FormApi.md)
- [AnyDeepKeyAndValue](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/AnyDeepKeyAndValue.md)
- [ArrayDeepKeyAndValue](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/ArrayDeepKeyAndValue.md)
- [BaseFormOptions](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/BaseFormOptions.md)
- [FieldApiOptions](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FieldApiOptions.md)
- [FieldGroupOptions](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FieldGroupOptions.md)
- [FieldGroupState](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FieldGroupState.md)
- [FieldListeners](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FieldListeners.md)
- [FieldOptions](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FieldOptions.md)
- [FieldValidators](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FieldValidators.md)
- [FormListeners](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FormListeners.md)
- [FormOptions](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FormOptions.md)
- [FormState](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FormState.md)
- [FormValidators](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/FormValidators.md)
- [ObjectDeepKeyAndValue](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/ObjectDeepKeyAndValue.md)
- [StandardSchemaV1Issue](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/StandardSchemaV1Issue.md)
- [TupleDeepKeyAndValue](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/TupleDeepKeyAndValue.md)
- [UnknownDeepKeyAndValue](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/UnknownDeepKeyAndValue.md)
- [ValidationLogicProps](https://github.com/tanstack/form/blob/main/docs/reference/interfaces/ValidationLogicProps.md)
- [AllObjectKeys](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AllObjectKeys.md)
- [AllTupleKeys](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AllTupleKeys.md)
- [AnyFieldApi](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFieldApi.md)
- [AnyFieldGroupApi](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFieldGroupApi.md)
- [AnyFieldMeta](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFieldMeta.md)
- [AnyFieldMetaBase](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFieldMetaBase.md)
- [AnyFieldMetaDerived](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFieldMetaDerived.md)
- [AnyFormApi](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFormApi.md)
- [AnyFormOptions](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFormOptions.md)
- [AnyFormState](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/AnyFormState.md)
- [ArrayAccessor](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ArrayAccessor.md)
- [BaseFormState](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/BaseFormState.md)
- [BroadcastFormApi](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/BroadcastFormApi.md)
- [BroadcastFormId](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/BroadcastFormId.md)
- [BroadcastFormState](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/BroadcastFormState.md)
- [BroadcastFormSubmissionState](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/BroadcastFormSubmissionState.md)
- [DeepKeyAndValueArray](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeyAndValueArray.md)
- [DeepKeyAndValueObject](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeyAndValueObject.md)
- [DeepKeyAndValueTuple](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeyAndValueTuple.md)
- [DeepKeys](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeys.md)
- [DeepKeysAndValues](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeysAndValues.md)
- [DeepKeysAndValuesImpl](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeysAndValuesImpl.md)
- [DeepKeysOfType](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepKeysOfType.md)
- [DeepRecord](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepRecord.md)
- [DeepValue](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DeepValue.md)
- [DerivedFormState](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/DerivedFormState.md)
- [EventClientEventMap](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/EventClientEventMap.md)
- [EventClientEventNames](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/EventClientEventNames.md)
- [ExtractGlobalFormError](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ExtractGlobalFormError.md)
- [FieldInfo](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FieldInfo.md)
- [FieldMeta](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FieldMeta.md)
- [FieldMetaBase](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FieldMetaBase.md)
- [FieldMetaDerived](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FieldMetaDerived.md)
- [FieldsMap](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FieldsMap.md)
- [FieldState](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FieldState.md)
- [FormValidateFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FormValidateFn.md)
- [FormValidationError](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FormValidationError.md)
- [FormValidator](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/FormValidator.md)
- [Nullable](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/Nullable.md)
- [ObjectAccessor](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ObjectAccessor.md)
- [ObjectValue](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ObjectValue.md)
- [StandardSchemaV1](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/StandardSchemaV1.md)
- [TStandardSchemaValidatorIssue](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/TStandardSchemaValidatorIssue.md)
- [TStandardSchemaValidatorValue](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/TStandardSchemaValidatorValue.md)
- [TupleAccessor](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/TupleAccessor.md)
- [UnknownAccessor](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/UnknownAccessor.md)
- [UnwrapFieldAsyncValidateOrFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/UnwrapFieldAsyncValidateOrFn.md)
- [UnwrapFieldValidateOrFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/UnwrapFieldValidateOrFn.md)
- [UnwrapFormAsyncValidateOrFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/UnwrapFormAsyncValidateOrFn.md)
- [UnwrapFormValidateOrFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/UnwrapFormValidateOrFn.md)
- [Updater](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/Updater.md)
- [UpdaterFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/UpdaterFn.md)
- [ValidationError](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ValidationError.md)
- [ValidationLogicFn](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ValidationLogicFn.md)
- [ValidationMeta](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ValidationMeta.md)
- [ValidationSource](https://github.com/tanstack/form/blob/main/docs/reference/type-aliases/ValidationSource.md)
- [defaultValidationLogic](https://github.com/tanstack/form/blob/main/docs/reference/variables/defaultValidationLogic.md)
- [formEventClient](https://github.com/tanstack/form/blob/main/docs/reference/variables/formEventClient.md)
- [standardSchemaValidators](https://github.com/tanstack/form/blob/main/docs/reference/variables/standardSchemaValidators.md)
- [throttleFormState](https://github.com/tanstack/form/blob/main/docs/reference/variables/throttleFormState.md)
- [createFieldMap](https://github.com/tanstack/form/blob/main/docs/reference/functions/createFieldMap.md)
- [evaluate](https://github.com/tanstack/form/blob/main/docs/reference/functions/evaluate.md)
- [formOptions](https://github.com/tanstack/form/blob/main/docs/reference/functions/formOptions.md)
- [isGlobalFormValidationError](https://github.com/tanstack/form/blob/main/docs/reference/functions/isGlobalFormValidationError.md)
- [isStandardSchemaValidator](https://github.com/tanstack/form/blob/main/docs/reference/functions/isStandardSchemaValidator.md)
- [mergeForm](https://github.com/tanstack/form/blob/main/docs/reference/functions/mergeForm.md)
- [revalidateLogic](https://github.com/tanstack/form/blob/main/docs/reference/functions/revalidateLogic.md)
- [uuid](https://github.com/tanstack/form/blob/main/docs/reference/functions/uuid.md)

--- docs/framework/angular/reference/index.md ---
---
id: "@tanstack/angular-form"
title: "@tanstack/angular-form"
---

# @tanstack/angular-form

## Classes

- [TanStackAppField](classes/TanStackAppField.md)
- [TanStackField](classes/TanStackField.md)
- [TanStackFieldInjectable](classes/TanStackFieldInjectable.md)

## Functions

- [injectField](functions/injectField.md)
- [injectForm](functions/injectForm.md)
- [injectStore](functions/injectStore.md)


## Links discovered
- [TanStackAppField](https://github.com/tanstack/form/blob/main/docs/framework/angular/reference/classes/TanStackAppField.md)
- [TanStackField](https://github.com/tanstack/form/blob/main/docs/framework/angular/reference/classes/TanStackField.md)
- [TanStackFieldInjectable](https://github.com/tanstack/form/blob/main/docs/framework/angular/reference/classes/TanStackFieldInjectable.md)
- [injectField](https://github.com/tanstack/form/blob/main/docs/framework/angular/reference/functions/injectField.md)
- [injectForm](https://github.com/tanstack/form/blob/main/docs/framework/angular/reference/functions/injectForm.md)
- [injectStore](https://github.com/tanstack/form/blob/main/docs/framework/angular/reference/functions/injectStore.md)

--- docs/framework/lit/reference/index.md ---
---
id: "@tanstack/lit-form"
title: "@tanstack/lit-form"
---

# @tanstack/lit-form

## Classes

- [TanStackFormController](classes/TanStackFormController.md)


## Links discovered
- [TanStackFormController](https://github.com/tanstack/form/blob/main/docs/framework/lit/reference/classes/TanStackFormController.md)

--- docs/framework/react/reference/index.md ---
---
id: "@tanstack/react-form"
title: "@tanstack/react-form"
---

# @tanstack/react-form

## Interfaces

- [ReactFormApi](interfaces/ReactFormApi.md)
- [UseFieldOptions](interfaces/UseFieldOptions.md)
- [UseFieldOptionsBound](interfaces/UseFieldOptionsBound.md)
- [WithFieldGroupProps](interfaces/WithFieldGroupProps.md)
- [WithFormProps](interfaces/WithFormProps.md)

## Type Aliases

- [FieldComponent](type-aliases/FieldComponent.md)
- [LensFieldComponent](type-aliases/LensFieldComponent.md)
- [ReactFormExtendedApi](type-aliases/ReactFormExtendedApi.md)
- [ServerFormState](type-aliases/ServerFormState.md)
- [UseField](type-aliases/UseField.md)

## Variables

- [Field](variables/Field.md)

## Functions

- [createFormHook](functions/createFormHook.md)
- [createFormHookContexts](functions/createFormHookContexts.md)
- [useField](functions/useField.md)
- [useFieldGroup](functions/useFieldGroup.md)
- [useForm](functions/useForm.md)
- [useStore](functions/useStore.md)


## Links discovered
- [ReactFormApi](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/interfaces/ReactFormApi.md)
- [UseFieldOptions](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/interfaces/UseFieldOptions.md)
- [UseFieldOptionsBound](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/interfaces/UseFieldOptionsBound.md)
- [WithFieldGroupProps](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/interfaces/WithFieldGroupProps.md)
- [WithFormProps](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/interfaces/WithFormProps.md)
- [FieldComponent](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/type-aliases/FieldComponent.md)
- [LensFieldComponent](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/type-aliases/LensFieldComponent.md)
- [ReactFormExtendedApi](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/type-aliases/ReactFormExtendedApi.md)
- [ServerFormState](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/type-aliases/ServerFormState.md)
- [UseField](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/type-aliases/UseField.md)
- [Field](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/variables/Field.md)
- [createFormHook](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/functions/createFormHook.md)
- [createFormHookContexts](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/functions/createFormHookContexts.md)
- [useField](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/functions/useField.md)
- [useFieldGroup](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/functions/useFieldGroup.md)
- [useForm](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/functions/useForm.md)
- [useStore](https://github.com/tanstack/form/blob/main/docs/framework/react/reference/functions/useStore.md)

--- docs/framework/solid/reference/index.md ---
---
id: "@tanstack/solid-form"
title: "@tanstack/solid-form"
---

# @tanstack/solid-form

## Interfaces

- [CreateFieldOptions](interfaces/CreateFieldOptions.md)
- [CreateFieldOptionsBound](interfaces/CreateFieldOptionsBound.md)
- [SolidFormApi](interfaces/SolidFormApi.md)
- [WithFieldGroupProps](interfaces/WithFieldGroupProps.md)
- [WithFormProps](interfaces/WithFormProps.md)

## Type Aliases

- [CreateField](type-aliases/CreateField.md)
- [FieldComponent](type-aliases/FieldComponent.md)
- [LensFieldComponent](type-aliases/LensFieldComponent.md)
- [SolidFormExtendedApi](type-aliases/SolidFormExtendedApi.md)

## Functions

- [createField](functions/createField.md)
- [createFieldGroup](functions/createFieldGroup.md)
- [createForm](functions/createForm.md)
- [createFormHook](functions/createFormHook.md)
- [createFormHookContexts](functions/createFormHookContexts.md)
- [Field](functions/Field.md)
- [useStore](functions/useStore.md)


## Links discovered
- [CreateFieldOptions](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/interfaces/CreateFieldOptions.md)
- [CreateFieldOptionsBound](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/interfaces/CreateFieldOptionsBound.md)
- [SolidFormApi](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/interfaces/SolidFormApi.md)
- [WithFieldGroupProps](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/interfaces/WithFieldGroupProps.md)
- [WithFormProps](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/interfaces/WithFormProps.md)
- [CreateField](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/type-aliases/CreateField.md)
- [FieldComponent](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/type-aliases/FieldComponent.md)
- [LensFieldComponent](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/type-aliases/LensFieldComponent.md)
- [SolidFormExtendedApi](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/type-aliases/SolidFormExtendedApi.md)
- [createField](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/createField.md)
- [createFieldGroup](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/createFieldGroup.md)
- [createForm](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/createForm.md)
- [createFormHook](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/createFormHook.md)
- [createFormHookContexts](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/createFormHookContexts.md)
- [Field](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/Field.md)
- [useStore](https://github.com/tanstack/form/blob/main/docs/framework/solid/reference/functions/useStore.md)

--- docs/framework/vue/reference/index.md ---
---
id: "@tanstack/vue-form"
title: "@tanstack/vue-form"
---

# @tanstack/vue-form

## Interfaces

- [VueFieldApi](interfaces/VueFieldApi.md)
- [VueFormApi](interfaces/VueFormApi.md)

## Type Aliases

- [FieldComponent](type-aliases/FieldComponent.md)
- [FieldComponentBoundProps](type-aliases/FieldComponentBoundProps.md)
- [FieldComponentProps](type-aliases/FieldComponentProps.md)
- [UseField](type-aliases/UseField.md)

## Variables

- [Field](variables/Field.md)

## Functions

- [useField](functions/useField.md)
- [useForm](functions/useForm.md)
- [useStore](functions/useStore.md)


## Links discovered
- [VueFieldApi](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/interfaces/VueFieldApi.md)
- [VueFormApi](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/interfaces/VueFormApi.md)
- [FieldComponent](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/type-aliases/FieldComponent.md)
- [FieldComponentBoundProps](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/type-aliases/FieldComponentBoundProps.md)
- [FieldComponentProps](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/type-aliases/FieldComponentProps.md)
- [UseField](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/type-aliases/UseField.md)
- [Field](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/variables/Field.md)
- [useField](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/functions/useField.md)
- [useForm](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/functions/useForm.md)
- [useStore](https://github.com/tanstack/form/blob/main/docs/framework/vue/reference/functions/useStore.md)

--- docs/community-resources.md ---
---
title: Community Resources
media:
  [
    {
      title: "Balastrong's TanStack Form Video Series",
      url: 'https://www.youtube.com/playlist?list=PLOQjd5dsGSxInTKUWTxyqSKwZCjDIUs0Y',
      description: 'Series by Balastrong, a maintainer of TanStack Form, covering setup, validation, array fields, reactivity, schema libraries, side effects, and composable fields.',
    },
    {
      title: 'TanStack Form Tutorial - Best Form Library for React?',
      url: 'https://youtu.be/5oFQd-uAAHo',
      description: 'A tutorial from Atharva Deosthale using TanStack Form in a Next.js project. Made for those just getting started with TanStack Form, covering client-side and server-side form validation.',
    },
  ]
---


--- docs/comparison.md ---
---
id: comparison
title: Comparison | TanStack Form
---

> ⚠️ This comparison table is under construction and is still not completely accurate. If you use any of these libraries and feel the information could be improved, feel free to suggest changes (with notes or evidence of claims) using the "Edit this page on Github" link at the bottom of this page.

Feature/Capability Key:

- ✅ 1st-class, built-in, and ready to use with no added configuration or code
- 🟡 Supported, but as an unofficial 3rd party or community library/contribution
- 🔶 Supported and documented, but requires extra user-code to implement
- 🛑 Not officially supported or documented.

| Feature                                           | TanStack Form                                | Formik                         | Redux Form                             | React Hook Form                                  | Final Form                             |
| ------------------------------------------------- | -------------------------------------------- | ------------------------------ | -------------------------------------- | ------------------------------------------------ | -------------------------------------- |
| Github Repo / Stars                               | [![][stars-tanstack-form]][gh-tanstack-form] | [![][stars-formik]][gh-formik] | [![][stars-redux-form]][gh-redux-form] | [![][stars-react-hook-form]][gh-react-hook-form] | [![][stars-final-form]][gh-final-form] |
| Supported Frameworks                              | React, Vue, Angular, Solid, Lit              | React                          | React                                  | React                                            | React, Vue, Angular, Solid, Vanilla JS |
| Bundle Size                                       | [![][bp-tanstack-form]][bpl-tanstack-form]   | [![][bp-formik]][bpl-formik]   | [![][bp-redux-form]][bpl-redux-form]   | [![][bp-react-hook-form]][bpl-react-hook-form]   | [![][bp-final-form]][bpl-final-form]   |
| First-class TypeScript support                    | ✅                                           | ❓                             | ❓                                     | ✅                                               | ✅                                     |
| Fully Inferred TypeScript (Including Deep Fields) | ✅                                           | ❓                             | ❓                                     | ✅                                               | 🛑                                     |
| Headless UI components                            | ✅                                           | ❓                             | ❓                                     | ✅                                               | ❓                                     |
| Framework agnostic                                | ✅                                           | ❓                             | ❓                                     | 🛑                                               | ✅                                     |
| Granular reactivity                               | ✅                                           | ❓                             | ❓                                     | ❓                                               | ✅                                     |
| Nested object/array fields                        | ✅                                           | ✅                             | ❓                                     | ✅\*(1)                                          | ✅                                     |
| Async validation                                  | ✅                                           | ✅                             | ❓                                     | ✅                                               | ✅                                     |
| Built-in async validation debounce                | ✅                                           | ❓                             | ❓                                     | ❓                                               | ❓                                     |
| Schema-based Validation                           | ✅                                           | ✅                             | ❓                                     | ✅                                               | ❓                                     |
| First Party Devtools                              | 🛑\*(2)                                      | 🛑                             | ✅\*(3)                                | ✅                                               | ❓                                     |
| SSR integrations                                  | ✅                                           | 🛑                             | 🛑                                     | 🛑                                               | 🛑                                     |
| React Compiler support                            | ✅                                           | ❓                             | ❓                                     | 🛑                                               | ❓                                     |

\*(1) For nested arrays, react-hook-form requires you [to cast the field array by its name](https://react-hook-form.com/docs/usefieldarray) if you're using TypeScript

\*(2) Planned

\*(3) Via Redux Devtools

[bpl-tanstack-form]: https://bundlephobia.com/result?p=@tanstack/react-form
[bp-tanstack-form]: https://badgen.net/bundlephobia/minzip/@tanstack/react-form?label=💾
[gh-tanstack-form]: https://github.com/TanStack/form
[stars-tanstack-form]: https://img.shields.io/github/stars/TanStack/form?label=%F0%9F%8C%9F
[bpl-formik]: https://bundlephobia.com/result?p=formik
[bp-formik]: https://badgen.net/bundlephobia/minzip/formik?label=💾
[gh-formik]: https://github.com/jaredpalmer/formik
[stars-formik]: https://img.shields.io/github/stars/jaredpalmer/formik?label=%F0%9F%8C%9F
[bpl-redux-form]: https://bundlephobia.com/result?p=redux-form
[bp-redux-form]: https://badgen.net/bundlephobia/minzip/redux-form?label=💾
[gh-redux-form]: https://github.com/redux-form/redux-form
[stars-redux-form]: https://img.shields.io/github/stars/redux-form/redux-form?label=%F0%9F%8C%9F
[bpl-react-hook-form]: https://bundlephobia.com/result?p=react-hook-form
[bp-react-hook-form]: https://badgen.net/bundlephobia/minzip/react-hook-form?label=💾
[gh-react-hook-form]: https://github.com/react-hook-form/react-hook-form
[stars-react-hook-form]: https://img.shields.io/github/stars/react-hook-form/react-hook-form?label=%F0%9F%8C%9F
[bpl-final-form]: https://bundlephobia.com/result?p=final-form
[bp-final-form]: https://badgen.net/bundlephobia/minzip/final-form?label=💾
[gh-final-form]: https://github.com/final-form/final-form
[stars-final-form]: https://img.shields.io/github/stars/final-form/final-form?label=%F0%9F%8C%9F


## Links discovered
- [to cast the field array by its name](https://react-hook-form.com/docs/usefieldarray)

--- examples/angular/array/README.md ---
# Simple

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.

## Code scaffolding

Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.

## Build

Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).

## Running end-to-end tests

Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.

## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.


## Links discovered
- [Angular CLI](https://github.com/angular/angular-cli)
- [Karma](https://karma-runner.github.io)
- [Angular CLI Overview and Command Reference](https://angular.io/cli)

--- examples/angular/large-form/README.md ---
# Simple

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.

## Code scaffolding

Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.

## Build

Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).

## Running end-to-end tests

Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.

## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.


## Links discovered
- [Angular CLI](https://github.com/angular/angular-cli)
- [Karma](https://karma-runner.github.io)
- [Angular CLI Overview and Command Reference](https://angular.io/cli)

--- examples/angular/simple/README.md ---
# Simple

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.

## Code scaffolding

Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.

## Build

Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).

## Running end-to-end tests

Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.

## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.


## Links discovered
- [Angular CLI](https://github.com/angular/angular-cli)
- [Karma](https://karma-runner.github.io)
- [Angular CLI Overview and Command Reference](https://angular.io/cli)

--- examples/angular/standard-schema/README.md ---
# Simple

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.

## Code scaffolding

Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.

## Build

Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).

## Running end-to-end tests

Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.

## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.


## Links discovered
- [Angular CLI](https://github.com/angular/angular-cli)
- [Karma](https://karma-runner.github.io)
- [Angular CLI Overview and Command Reference](https://angular.io/cli)

--- examples/lit/array/README.md ---
# Example

To run this example:

- `npm install`
- `npm run dev`


--- examples/lit/simple/README.md ---
# Example

To run this example:

- `npm install`
- `npm run dev`


--- examples/lit/standard-schema/README.md ---
# Example

To run this example:

- `npm install`
- `npm run dev`


--- examples/lit/ui-libraries/README.md ---
# Example

To run this example:

- `npm install`
- `npm run dev`


--- examples/react/array/README.md ---
# Example

To run this example:

- `npm install`
- `npm run dev`


--- examples/react/compiler/README.md ---
# Example

To run this example:

- `npm install`
- `npm run dev`


--- packages/form-core/src/FieldApi.ts ---
import { Derived, batch } from '@tanstack/store'
import {
  isStandardSchemaValidator,
  standardSchemaValidators,
} from './standardSchemaValidator'
import { defaultFieldMeta } from './metaHelper'
import {
  determineFieldLevelErrorSourceAndValue,
  evaluate,
  getAsyncValidatorArray,
  getBy,
  getSyncValidatorArray,
  mergeOpts,
} from './utils'
import { defaultValidationLogic } from './ValidationLogic'
import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types'
import type {
  StandardSchemaV1,
  StandardSchemaV1Issue,
  TStandardSchemaValidatorValue,
} from './standardSchemaValidator'
import type {
  FieldInfo,
  FormApi,
  FormAsyncValidateOrFn,
  FormValidateAsyncFn,
  FormValidateFn,
  FormValidateOrFn,
} from './FormApi'
import type {
  ListenerCause,
  UpdateMetaOptions,
  ValidationCause,
  ValidationError,
  ValidationErrorMap,
  ValidationErrorMapSource,
} from './types'
import type { AsyncValidator, SyncValidator, Updater } from './utils'

/**
 * @private
 */
// TODO: Add the `Unwrap` type to the errors
type FieldErrorMapFromValidator<
  TFormData,
  TName extends DeepKeys<TFormData>,
  TData extends DeepValue<TFormData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TFormData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TFormData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TFormData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TFormData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TFormData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TFormData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TFormData, TName, TData>,
> = Partial<
  Record<
    DeepKeys<TFormData>,
    ValidationErrorMap<
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync
    >
  >
>

/**
 * @private
 */
export type FieldValidateFn<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> = (props: {
  value: TData
  fieldApi: FieldApi<
    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 field 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
  >
}) => unknown

/**
 * @private
 */
export type FieldValidateOrFn<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> =
  | FieldValidateFn<TParentData, TName, TData>
  | StandardSchemaV1<TData, unknown>

type StandardBrandedSchemaV1<T> = T & { __standardSchemaV1: true }

type UnwrapFormValidateOrFnForInner<
  TValidateOrFn extends undefined | FormValidateOrFn<any>,
> = [TValidateOrFn] extends [FormValidateFn<any>]
  ? ReturnType<TValidateOrFn>
  : [TValidateOrFn] extends [StandardSchemaV1<infer TOut, any>]
    ? StandardBrandedSchemaV1<TOut>
    : undefined

export type UnwrapFieldValidateOrFn<
  TName extends string,
  TValidateOrFn extends undefined | FieldValidateOrFn<any, any, any>,
  TFormValidateOrFn extends undefined | FormValidateOrFn<any>,
> =
  | ([TFormValidateOrFn] extends [StandardSchemaV1<any, infer TStandardOut>]
      ? TName extends keyof TStandardOut
        ? StandardSchemaV1Issue[]
        : undefined
      : undefined)
  | (UnwrapFormValidateOrFnForInner<TFormValidateOrFn> extends infer TFormValidateVal
      ? TFormValidateVal extends { __standardSchemaV1: true }
        ? [DeepValue<TFormValidateVal, TName>] extends [never]
          ? undefined
          : StandardSchemaV1Issue[]
        : TFormValidateVal extends { fields: any }
          ? TName extends keyof TFormValidateVal['fields']
            ? TFormValidateVal['fields'][TName]
            : undefined
          : undefined
      : never)
  | ([TValidateOrFn] extends [FieldValidateFn<any, any, any>]
      ? ReturnType<TValidateOrFn>
      : [TValidateOrFn] extends [StandardSchemaV1<any, any>]
        ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][]
          StandardSchemaV1Issue[]
        : undefined)

/**
 * @private
 */
export type FieldValidateAsyncFn<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> = (options: {
  value: TData
  fieldApi: FieldApi<
    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 field 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
  >
  signal: AbortSignal
}) => unknown | Promise<unknown>

/**
 * @private
 */
export type FieldAsyncValidateOrFn<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> =
  | FieldValidateAsyncFn<TParentData, TName, TData>
  | StandardSchemaV1<TData, unknown>

type UnwrapFormAsyncValidateOrFnForInner<
  TValidateOrFn extends undefined | FormAsyncValidateOrFn<any>,
> = [TValidateOrFn] extends [FormValidateAsyncFn<any>]
  ? Awaited<ReturnType<TValidateOrFn>>
  : [TValidateOrFn] extends [StandardSchemaV1<infer TOut, any>]
    ? StandardBrandedSchemaV1<TOut>
    : undefined

export type UnwrapFieldAsyncValidateOrFn<
  TName extends string,
  TValidateOrFn extends undefined | FieldAsyncValidateOrFn<any, any, any>,
  TFormValidateOrFn extends undefined | FormAsyncValidateOrFn<any>,
> =
  | ([TFormValidateOrFn] extends [StandardSchemaV1<any, infer TStandardOut>]
      ? TName extends keyof TStandardOut
        ? StandardSchemaV1Issue[]
        : undefined
      : undefined)
  | (UnwrapFormAsyncValidateOrFnForInner<TFormValidateOrFn> extends infer TFormValidateVal
      ? TFormValidateVal extends { __standardSchemaV1: true }
        ? [DeepValue<TFormValidateVal, TName>] extends [never]
          ? undefined
          : StandardSchemaV1Issue[]
        : TFormValidateVal extends { fields: any }
          ? TName extends keyof TFormValidateVal['fields']
            ? TFormValidateVal['fields'][TName]
            : undefined
          : undefined
      : never)
  | ([TValidateOrFn] extends [FieldValidateAsyncFn<any, any, any>]
      ? Awaited<ReturnType<TValidateOrFn>>
      : [TValidateOrFn] extends [StandardSchemaV1<any, any>]
        ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][]
          StandardSchemaV1Issue[]
        : undefined)

/**
 * @private
 */
export type FieldListenerFn<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> = (props: {
  value: TData
  fieldApi: FieldApi<
    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 field 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
  >
}) => void

export interface FieldValidators<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
> {
  /**
   * 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<TParentData>[]
  /**
   * 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<TParentData>[]
  /**
   * 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 FieldListeners<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> {
  onChange?: FieldListenerFn<TParentData, TName, TData>
  onChangeDebounceMs?: number
  onBlur?: FieldListenerFn<TParentData, TName, TData>
  onBlurDebounceMs?: number
  onMount?: FieldListenerFn<TParentData, TName, TData>
  onSubmit?: FieldListenerFn<TParentData, TName, TData>
}

/**
 * An object type representing the options for a field in a form.
 */
export interface FieldOptions<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
> {
  /**
   * The field name. The type will be `DeepKeys<TParentData>` to ensure your name is a deep key of the parent dataset.
   */
  name: TName
  /**
   * An optional default value for the field.
   */
  defaultValue?: NoInfer<TData>
  /**
   * The default time to debounce async validation if there is not a more specific debounce time passed.
   */
  asyncDebounceMs?: number
  /**
   * If `true`, always run async validation, even if there are errors emitted during synchronous validation.
   */
  asyncAlways?: boolean
  /**
   * A list of validators to pass to the field
   */
  validators?: FieldValidators<
    TParentData,
    TName,
    TData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync
  >
  /**
   * An optional object with default metadata for the field.
   */
  defaultMeta?: Partial<
    FieldMeta<
      TParentData,
      TName,
      TData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any
    >
  >
  /**
   * A list of listeners which attach to the corresponding events
   */
  listeners?: FieldListeners<TParentData, TName, TData>
  /**
   * Disable the `flat(1)` operation on `field.errors`. This is useful if you want to keep the error structure as is. Not suggested for most use-cases.
   */
  disableErrorFlat?: boolean
}

/**
 * An object type representing the required options for the FieldApi class.
 */
export interface FieldApiOptions<
  in out TParentData,
  in out TName extends DeepKeys<TParentData>,
  in out TData extends DeepValue<TParentData, TName>,
  in out TOnMount extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnChange extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TOnBlur extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TOnSubmit extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TOnDynamic extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnChangeAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnBlurAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnSubmitAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnDynamicAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>,
  in out TParentSubmitMeta,
> extends FieldOptions<
  TParentData,
  TName,
  TData,
  TOnMount,
  TOnChange,
  TOnChangeAsync,
  TOnBlur,
  TOnBlurAsync,
  TOnSubmit,
  TOnSubmitAsync,
  TOnDynamic,
  TOnDynamicAsync
> {
  form: FormApi<
    TParentData,
    TFormOnMount,
    TFormOnChange,
    TFormOnChangeAsync,
    TFormOnBlur,
    TFormOnBlurAsync,
    TFormOnSubmit,
    TFormOnSubmitAsync,
    TFormOnDynamic,
    TFormOnDynamicAsync,
    TFormOnServer,
    TParentSubmitMeta
  >
}

export type FieldMetaBase<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
> = {
  /**
   * A flag indicating whether the field has been touched.
   */
  isTouched: boolean
  /**
   * A flag indicating whether the field has been blurred.
   */
  isBlurred: boolean
  /**
   * A flag that is `true` if the field's value has been modified by the user. Opposite of `isPristine`.
   */
  isDirty: boolean
  /**
   * A map of errors related to the field value.
   */
  errorMap: ValidationErrorMap<
    UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>,
    UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>,
    UnwrapFieldAsyncValidateOrFn<TName, TOnChangeAsync, TFormOnChangeAsync>,
    UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>,
    UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>,
    UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>,
    UnwrapFieldAsyncValidateOrFn<TName, TOnSubmitAsync, TFormOnSubmitAsync>,
    UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>,
    UnwrapFieldAsyncValidateOrFn<TName, TOnDynamicAsync, TFormOnDynamicAsync>
  >

  /**
   * @private allows tracking the source of the errors in the error map
   */
  errorSourceMap: ValidationErrorMapSource
  /**
   * A flag indicating whether the field is currently being validated.
   */
  isValidating: boolean
}

export type AnyFieldMetaBase = FieldMetaBase<
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any
>

export type FieldMetaDerived<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
> = {
  /**
   * An array of errors related to the field value.
   */
  errors: Array<
    | UnwrapOneLevelOfArray<
        UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldAsyncValidateOrFn<TName, TOnChangeAsync, TFormOnChangeAsync>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldAsyncValidateOrFn<TName, TOnSubmitAsync, TFormOnSubmitAsync>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>
      >
    | UnwrapOneLevelOfArray<
        UnwrapFieldAsyncValidateOrFn<
          TName,
          TOnDynamicAsync,
          TFormOnDynamicAsync
        >
      >
  >
  /**
   * A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`.
   */
  isPristine: boolean
  /**
   * A boolean indicating if the field is valid. Evaluates `true` if there are no field errors.
   */
  isValid: boolean
  /**
   * A flag indicating whether the field's current value is the default value
   */
  isDefaultValue: boolean
}

export type AnyFieldMetaDerived = FieldMetaDerived<
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any
>

/**
 * An object type representing the metadata of a field in a form.
 */
export type FieldMeta<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
> = FieldMetaBase<
  TParentData,
  TName,
  TData,
  TOnMount,
  TOnChange,
  TOnChangeAsync,
  TOnBlur,
  TOnBlurAsync,
  TOnSubmit,
  TOnSubmitAsync,
  TOnDynamic,
  TOnDynamicAsync,
  TFormOnMount,
  TFormOnChange,
  TFormOnChangeAsync,
  TFormOnBlur,
  TFormOnBlurAsync,
  TFormOnSubmit,
  TFormOnSubmitAsync,
  TFormOnDynamic,
  TFormOnDynamicAsync
> &
  FieldMetaDerived<
    TParentData,
    TName,
    TData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TFormOnMount,
    TFormOnChange,
    TFormOnChangeAsync,
    TFormOnBlur,
    TFormOnBlurAsync,
    TFormOnSubmit,
    TFormOnSubmitAsync,
    TFormOnDynamic,
    TFormOnDynamicAsync
  >

export type AnyFieldMeta = FieldMeta<
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any
>

/**
 * An object type representing the state of a field.
 */
export type FieldState<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TData extends DeepValue<TParentData, TName>,
  TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
  TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
> = {
  /**
   * The current value of the field.
   */
  value: TData
  /**
   * The current metadata of the field.
   */
  meta: FieldMeta<
    TParentData,
    TName,
    TData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TFormOnMount,
    TFormOnChange,
    TFormOnChangeAsync,
    TFormOnBlur,
    TFormOnBlurAsync,
    TFormOnSubmit,
    TFormOnSubmitAsync,
    TFormOnDynamic,
    TFormOnDynamicAsync
  >
}

/**
 * @public
 *
 * A type representing the Field API with all generics set to `any` for convenience.
 */
export type AnyFieldApi = FieldApi<
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any
>

/**
 * 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 API for managing a form field.
 *
 * Normally, you will not need to create a new `FieldApi` instance directly.
 * Instead, you will use a framework hook/function like `useField` or `createField`
 * 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 FieldApi` constructor.
 */
export class FieldApi<
  in out TParentData,
  in out TName extends DeepKeys<TParentData>,
  in out TData extends DeepValue<TParentData, TName>,
  in out TOnMount extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnChange extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnChangeAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TOnBlur extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnBlurAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TOnSubmit extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnSubmitAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TOnDynamic extends
    | undefined
    | FieldValidateOrFn<TParentData, TName, TData>,
  in out TOnDynamicAsync extends
    | undefined
    | FieldAsyncValidateOrFn<TParentData, TName, TData>,
  in out TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnChangeAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnBlurAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnSubmitAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  in out TFormOnDynamicAsync extends
    | undefined
    | FormAsyncValidateOrFn<TParentData>,
  in out TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>,
  in out TParentSubmitMeta,
> {
  /**
   * A reference to the form API instance.
   */
  form: FieldApiOptions<
    TParentData,
    TName,
    TData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TFormOnMount,
    TFormOnChange,
    TFormOnChangeAsync,
    TFormOnBlur,
    TFormOnBlurAsync,
    TFormOnSubmit,
    TFormOnSubmitAsync,
    TFormOnDynamic,
    TFormOnDynamicAsync,
    TFormOnServer,
    TParentSubmitMeta
  >['form']
  /**
   * The field name.
   */
  name: TName
  /**
   * The field options.
   */
  options: FieldApiOptions<
    TParentData,
    TName,
    TData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TFormOnMount,
    TFormOnChange,
    TFormOnChangeAsync,
    TFormOnBlur,
    TFormOnBlurAsync,
    TFormOnSubmit,
    TFormOnSubmitAsync,
    TFormOnDynamic,
    TFormOnDynamicAsync,
    TFormOnServer,
    TParentSubmitMeta
  > = {} as any
  /**
   * The field state store.
   */
  store!: Derived<
    FieldState<
      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
  }
  timeoutIds: {
    validations: Record<ValidationCause, ReturnType<typeof setTimeout> | null>
    listeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
    formListeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
  }

  /**
   * Initializes a new `FieldApi` instance.
   */
  constructor(
    opts: FieldApiOptions<
      TParentData,
      TName,
      TData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      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<ValidationCause, never>,
      listeners: {} as Record<ListenerCause, never>,
      formListeners: {} as Record<ListenerCause, never>,
    }

    this.store = new Derived({
      deps: [this.form.store],
      fn: () => {
        const meta = this.form.getFieldMeta(this.name) ?? {
          ...defaultFieldMeta,
          ...opts.defaultMeta,
        }

        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
        }

        return {
          value,
          meta,
        } as FieldState<
          TParentData,
          TName,
          TData,
          TOnMount,
          TOnChange,
          TOnChangeAsync,
          TOnBlur,
          TOnBlurAsync,
          TOnSubmit,
          TOnSubmitAsync,
          TOnDynamic,
          TOnDynamicAsync,
          TFormOnMount,
          TFormOnChange,
          TFormOnChangeAsync,
          TFormOnBlur,
          TFormOnBlurAsync,
          TFormOnSubmit,
          TFormOnSubmitAsync,
          TFormOnDynamic,
          TFormOnDynamicAsync
        >
      },
    })
  }

  /**
   * @private
   */
  runValidator<
    TValue extends TStandardSchemaValidatorValue<TData> & {
      fieldApi: AnyFieldApi
    },
    TType extends 'validate' | 'validateAsync',
  >(props: {
    validate: TType extends 'validate'
      ? FieldValidateOrFn<any, any, any>
      : FieldAsyncValidateOrFn<any, any, any>
    value: TValue
    type: TType
    // When `api` is 'field', the return type cannot be `FormValidationError`
  }): unknown {
    if (isStandardSchemaValidator(props.validate)) {
      return standardSchemaValidators[props.type](
        props.value,
        props.validate,
      ) as never
    }

    return (props.validate as FieldValidateFn<any, any>)(props.value) as never
  }

  /**
   * Mounts the field instance to the form.
   */
  mount = () => {
    const cleanup = this.store.mount()

    if (this.options.defaultValue !== undefined && !this.getMeta().isTouched) {
      this.form.setFieldValue(this.name, this.options.defaultValue, {
        dontUpdateMeta: true,
      })
    }

    const info = this.getInfo()
    info.instance = this as never

    this.update(this.options as never)
    const { onMount } = this.options.validators || {}

    if (onMount) {
      const error = this.runValidator({
        validate: onMount,
        value: {
          value: this.state.value,
          fieldApi: this,
          validationSource: 'field',
        },
        type: 'validate',
      })
      if (error) {
        this.setMeta(
          (prev) =>
            ({
              ...prev,
              // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
              errorMap: { ...prev?.errorMap, onMount: error },
              errorSourceMap: {
                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
                ...prev?.errorSourceMap,
                onMount: 'field',
              },
            }) as never,
        )
      }
    }

    this.options.listeners?.onMount?.({
      value: this.state.value,
      fieldApi: this,
    })

    return cleanup
  }

  /**
   * Updates the field instance with new options.
   */
  update = (
    opts: FieldApiOptions<
      TParentData,
      TName,
      TData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      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, this.state.meta)
    }
  }

  /**
   * Gets the current field value.
   * @deprecated Use `field.state.value` instead.
   */
  getValue = (): TData => {
    return this.form.getFieldValue(this.name) as TData
  }

  /**
   * Sets the field value and run the `change` validator.
   */
  setValue = (updater: Updater<TData>, 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<
      FieldMetaBase<
        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.form.getFieldInfo(this.name)

  /**
   * Pushes a new value to the field.
   */
  pushValue = (
    value: TData extends any[] ? TData[number] : never,
    options?: UpdateMetaOptions,
  ) => {
    this.form.pushFieldValue(
      this.name,
      value as any,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * Inserts a value at the specified index, shifting the subsequent values to the right.
   */
  insertValue = (
    index: number,
    value: TData extends any[] ? TData[number] : never,
    options?: UpdateMetaOptions,
  ) => {
    this.form.insertFieldValue(
      this.name,
      index,
      value as any,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * Replaces a value at the specified index.
   */
  replaceValue = (
    index: number,
    value: TData extends any[] ? TData[number] : never,
    options?: UpdateMetaOptions,
  ) => {
    this.form.replaceFieldValue(
      this.name,
      index,
      value as any,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * Removes a value at the specified index.
   */
  removeValue = (index: number, options?: UpdateMetaOptions) => {
    this.form.removeFieldValue(
      this.name,
      index,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * Swaps the values at the specified indices.
   */
  swapValues = (
    aIndex: number,
    bIndex: number,
    options?: UpdateMetaOptions,
  ) => {
    this.form.swapFieldValues(
      this.name,
      aIndex,
      bIndex,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * Moves the value at the first specified index to the second specified index.
   */
  moveValue = (aIndex: number, bIndex: number, options?: UpdateMetaOptions) => {
    this.form.moveFieldValues(
      this.name,
      aIndex,
      bIndex,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * Clear all values from the array.
   */
  clearValues = (options?: UpdateMetaOptions) => {
    this.form.clearFieldValues(
      this.name,
      mergeOpts(options, { dontRunListeners: true }),
    )

    if (!options?.dontRunListeners) {
      this.triggerOnChangeListener()
    }
  }

  /**
   * @private
   */
  getLinkedFields = (cause: ValidationCause) => {
    const fields = Object.values(this.form.fieldInfo) as FieldInfo<any>[]

    const linkedFields: AnyFieldApi[] = []
    for (const field of fields) {
      if (!field.instance) continue
      const { onChangeListenTo, onBlurListenTo } =
        field.instance.options.validators || {}
      if (cause === 'change' && onChangeListenTo?.includes(this.name)) {
        linkedFields.push(field.instance)
      }
      if (cause === 'blur' && onBlurListenTo?.includes(this.name as string)) {
        linkedFields.push(field.instance)
      }
    }

    return linkedFields
  }

  /**
   * @private
   */
  validateSync = (
    cause: ValidationCause,
    errorFromForm: ValidationErrorMap,
  ) => {
    const validates = getSyncValidatorArray(cause, {
      ...this.options,
      form: this.form,
      validationLogic:
        this.form.options.validationLogic || defaultValidationLogic,
    })

    const linkedFields = this.getLinkedFields(cause)
    const linkedFieldValidates = linkedFields.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<any> & {
          field: AnyFieldApi
        }
      >,
    )

    // Needs type cast as eslint errantly believes this is always falsy
    let hasErrored = false as boolean

    batch(() => {
      const validateFieldFn = (
        field: AnyFieldApi,
        validateObj: SyncValidator<any>,
      ) => {
        const errorMapKey = getErrorMapKey(validateObj.cause)

        const fieldLevelError = validateObj.validate
          ? normalizeError(
              field.runValidator({
                validate: validateObj.validate,
                value: {
                  value: field.store.state.value,
                  validationSource: 'field',
                  fieldApi: field,
                },
                type: 'validate',
              }),
            )
          : undefined

        const formLevelError = errorFromForm[errorMapKey]

        const { newErrorValue, newSource } =
          determineFieldLevelErrorSourceAndValue({
            formLevelError,
            fieldLevelError,
          })

        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
        if (field.state.meta.errorMap?.[errorMapKey] !== newErrorValue) {
          field.setMeta((prev) => ({
            ...prev,
            errorMap: {
              ...prev.errorMap,
              [errorMapKey]: newErrorValue,
            },
            errorSourceMap: {
              ...prev.errorSourceMap,
              [errorMapKey]: newSource,
            },
          }))
        }
        if (newErrorValue) {
          hasErrored = true
        }
      }

      for (const validateObj of validates) {
        validateFieldFn(this, validateObj)
      }
      for (const fieldValitateObj of linkedFieldValidates) {
        if (!fieldValitateObj.validate) continue
        validateFieldFn(fieldValitateObj.field, fieldValitateObj)
      }
    })

    /**
     *  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
      >
    >,
  ) => {
    const validates = getAsyncValidatorArray(cause, {
      ...this.options,
      form: this.form,
      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 linkedFields = this.getLinkedFields(cause)
    const linkedFieldValidates = linkedFields.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<any> & {
          field: AnyFieldApi
        }
      >,
    )

    if (!this.state.meta.isValidating) {
      this.setMeta((prev) => ({ ...prev, isValidating: true }))
    }

    for (const linkedField of linkedFields) {
      linkedField.setMeta((prev) => ({ ...prev, isValidating: 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 validatesPromises: Promise<ValidationError | undefined>[] = []
    const linkedPromises: Promise<ValidationError | undefined>[] = []

    const validateFieldAsyncFn = (
      field: AnyFieldApi,
      validateObj: AsyncValidator<any>,
      promises: Promise<ValidationError | undefined>[],
    ) => {
      const errorMapKey = getErrorMapKey(validateObj.cause)
      const fieldValidatorMeta = field.getInfo().validationMetaMap[errorMapKey]

      fieldValidatorMeta?.lastAbortController.abort()
      const controller = new AbortController()

      this.getInfo().validationMetaMap[errorMapKey] = {
        lastAbortController: controller,
      }

      promises.push(
        new Promise<ValidationError | undefined>(async (resolve) => {
          let rawError!: ValidationError | undefined
          try {
            rawError = await new Promise((rawResolve, rawReject) => {
              if (this.timeoutIds.validations[validateObj.cause]) {
                clearTimeout(this.timeoutIds.validations[validateObj.cause]!)
              }

              this.timeoutIds.validations[validateObj.cause] = setTimeout(
                async () => {
                  if (controller.signal.aborted) return rawResolve(undefined)
                  try {
                    rawResolve(
                      await this.runValidator({
                        validate: validateObj.validate,
                        value: {
                          value: field.store.state.value,
                          fieldApi: field,
                          signal: controller.signal,
                          validationSource: 'field',
                        },
                        type: 'validateAsync',
                      }),
                    )
                  } catch (e) {
                    rawReject(e)
                  }
                },
                validateObj.debounceMs,
              )
            })
          } catch (e: unknown) {
            rawError = e as ValidationError
          }
          if (controller.signal.aborted) return resolve(undefined)

          const fieldLevelError = normalizeError(rawError)
          const formLevelError =
            asyncFormValidationResults[this.name]?.[errorMapKey]

          const { newErrorValue, newSource } =
            determineFieldLevelErrorSourceAndValue({
              formLevelError,
              fieldLevelError,
            })

          field.setMeta((prev) => {
            return {
              ...prev,
              errorMap: {
                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
                ...prev?.errorMap,
                [errorMapKey]: newErrorValue,
              },
              errorSourceMap: {
                ...prev.errorSourceMap,
                [errorMapKey]: newSource,
              },
            }
          })

          resolve(newErrorValue)
        }),
      )
    }

    // TODO: Dedupe this logic to reduce bundle size
    for (const validateObj of validates) {
      if (!validateObj.validate) continue
      validateFieldAsyncFn(this, validateObj, validatesPromises)
    }
    for (const fieldValitateObj of linkedFieldValidates) {
      if (!fieldValitateObj.validate) continue
      validateFieldAsyncFn(
        fieldValitateObj.field,
        fieldValitateObj,
        linkedPromises,
      )
    }

    let results: ValidationError[] = []
    if (validatesPromises.length || linkedPromises.length) {
      results = await Promise.all(validatesPromises)
      await Promise.all(linkedPromises)
    }

    this.setMeta((prev) => ({ ...prev, isValidating: false }))

    for (const linkedField of linkedFields) {
      linkedField.setMeta((prev) => ({ ...prev, isValidating: false }))
    }

    return results.filter(Boolean)
  }

  /**
   * Validates the field value.
   */
  validate = (
    cause: ValidationCause,
    opts?: { skipFormValidation?: boolean },
  ): ValidationError[] | Promise<ValidationError[]> => {
    // If the field is pristine, do not validate
    if (!this.state.meta.isTouched) return []

    // Attempt to sync validate first
    const { fieldsErrorMap } = opts?.skipFormValidation
      ? { fieldsErrorMap: {} as never }
      : this.form.validateSync(cause)
    const { hasErrored } = this.validateSync(
      cause,
      fieldsErrorMap[this.name] ?? {},
    )

    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)
    return this.validateAsync(cause, formValidationResultPromise)
  }

  /**
   * Handles the change event.
   */
  handleChange = (updater: Updater<TData>) => {
    this.setValue(updater)
  }

  /**
   * Handles the blur event.
   */
  handleBlur = () => {
    const prevTouched = this.state.meta.isTouched
    if (!prevTouched) {
      this.setMeta((prev) => ({ ...prev, isTouched: true }))
    }
    if (!this.state.meta.isBlurred) {
      this.setMeta((prev) => ({ ...prev, isBlurred: true }))
    }
    this.validate('blur')

    this.triggerOnBlurListener()
  }

  /**
   * Updates the field's errorMap
   */
  setErrorMap = (
    errorMap: ValidationErrorMap<
      UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>,
      UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>,
      UnwrapFieldAsyncValidateOrFn<TName, TOnChangeAsync, TFormOnChangeAsync>,
      UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>,
      UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>,
      UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>,
      UnwrapFieldAsyncValidateOrFn<TName, TOnSubmitAsync, TFormOnSubmitAsync>,
      UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>,
      UnwrapFieldAsyncValidateOrFn<TName, TOnDynamicAsync, TFormOnDynamicAsync>
    >,
  ) => {
    this.setMeta((prev) => ({
      ...prev,
      errorMap: {
        ...prev.errorMap,
        ...errorMap,
      },
    }))
  }

  /**
   * Parses the field's value with the given schema and returns
   * issues (if any). This method does NOT set any internal errors.
   * @param schema The standard schema to parse this field's value with.
   */
  parseValueWithSchema = (schema: StandardSchemaV1<TData, unknown>) => {
    return standardSchemaValidators.validate(
      { value: this.state.value, validationSource: 'field' },
      schema,
    )
  }

  /**
   * Parses the field's value with the given schema and returns
   * issues (if any). This method does NOT set any internal errors.
   * @param schema The standard schema to parse this field's value with.
   */
  parseValueWithSchemaAsync = (schema: StandardSchemaV1<TData, unknown>) => {
    return standardSchemaValidators.validateAsync(
      { value: this.state.value, validationSource: 'field' },
      schema,
    )
  }

  private triggerOnBlurListener() {
    const formDebounceMs = this.form.options.listeners?.onBlurDebounceMs
    if (formDebounceMs && formDebounceMs > 0) {
      if (this.timeoutIds.formListeners.blur) {
        clearTimeout(this.timeoutIds.formListeners.blur)
      }

      this.timeoutIds.formListeners.blur = setTimeout(() => {
        this.form.options.listeners?.onBlur?.({
          formApi: this.form,
          fieldApi: this,
        })
      }, formDebounceMs)
    } else {
      this.form.options.listeners?.onBlur?.({
        formApi: this.form,
        fieldApi: this,
      })
    }

    const fieldDebounceMs = this.options.listeners?.onBlurDebounceMs
    if (fieldDebounceMs && fieldDebounceMs > 0) {
      if (this.timeoutIds.listeners.blur) {
        clearTimeout(this.timeoutIds.listeners.blur)
      }

      this.timeoutIds.listeners.blur = setTimeout(() => {
        this.options.listeners?.onBlur?.({
          value: this.state.value,
          fieldApi: this,
        })
      }, fieldDebounceMs)
    } else {
      this.options.listeners?.onBlur?.({
        value: this.state.value,
        fieldApi: this,
      })
    }
  }

  /**
   * @private
   */
  triggerOnChangeListener = () => {
    const formDebounceMs = this.form.options.listeners?.onChangeDebounceMs
    if (formDebounceMs && formDebounceMs > 0) {
      if (this.timeoutIds.formListeners.change) {
        clearTimeout(this.timeoutIds.formListeners.change)
      }

      this.timeoutIds.formListeners.change = setTimeout(() => {
        this.form.options.listeners?.onChange?.({
          formApi: this.form,
          fieldApi: this,
        })
      }, formDebounceMs)
    } else {
      this.form.options.listeners?.onChange?.({
        formApi: this.form,
        fieldApi: 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,
          fieldApi: this,
        })
      }, fieldDebounceMs)
    } else {
      this.options.listeners?.onChange?.({
        value: this.state.value,
        fieldApi: this,
      })
    }
  }
}

function normalizeError(rawError?: ValidationError) {
  if (rawError) {
    return rawError
  }

  return 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'
  }
}


--- packages/form-core/tests/FieldApi.spec.ts ---
import { describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import { FieldApi, FormApi } from '../src/index'
import { sleep } from './utils'

describe('field api', () => {
  it('should have an initial value', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })

    field.mount()

    expect(field.getValue()).toBe('test')
  })

  it('should use field default value first', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      defaultValue: 'other',
      name: 'name',
    })

    field.mount()

    expect(field.getValue()).toBe('other')
  })

  it('should get default meta', () => {
    const form = new FormApi()

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })

    field.mount()

    expect(field.getMeta()).toEqual({
      isTouched: false,
      isBlurred: false,
      isDefaultValue: true,
      isValidating: false,
      isPristine: true,
      isValid: true,
      isDirty: false,
      errors: [],
      errorMap: {},
      errorSourceMap: {},
    })
  })

  it('should allow to set default meta', () => {
    const form = new FormApi()

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      defaultMeta: {
        isTouched: true,
        isBlurred: true,
        isDirty: true,
      },
    })

    field.mount()

    expect(field.getMeta()).toMatchObject({
      isTouched: true,
      isBlurred: true,
      isDirty: true,

      // derived meta data
      isValid: true,
      isValidating: false,
      errors: [],
      errorMap: {},
      errorSourceMap: {},
    })
  })

  it('should update the fields meta isDefaultValue with primitives', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()

    const field = new FieldApi({
      form: form,
      name: 'name',
      defaultValue: 'another-test',
    })
    field.mount()

    expect(field.getMeta().isDefaultValue).toBe(true)

    field.setValue('not-test')
    expect(field.getMeta().isDefaultValue).toBe(false)

    field.setValue('test')
    expect(field.getMeta().isDefaultValue).toBe(true)

    form.resetField('name')
    expect(field.getMeta().isDefaultValue).toBe(true)

    // checks the defaultValue provided to the field
    field.setValue('another-test')
    expect(field.getMeta().isDefaultValue).toBe(true)
  })

  it('should update the fields meta isDefaultValue with arrays - simple', () => {
    const form = new FormApi({
      defaultValues: {
        arr: ['', ''],
      },
    })
    form.mount()

    const field = new FieldApi({
      form: form,
      name: 'arr',
    })
    field.mount()

    expect(field.getMeta().isDefaultValue).toBe(true)

    field.setValue(['hello', 'goodbye'])
    expect(field.getMeta().isDefaultValue).toBe(false)

    field.setValue(['', ''])
    expect(field.getMeta().isDefaultValue).toBe(true)
  })

  it('should update the fields meta isDefaultValue with arrays - complex', () => {
    const defaultValues: [{ age: number; name?: string }, null | undefined] = [
      { age: 0 },
      undefined,
    ]
    const form = new FormApi({
      defaultValues: {
        arr: defaultValues,
      },
    })
    form.mount()

    const field = new FieldApi({
      form: form,
      name: 'arr',
    })

    field.mount()
    expect(field.getMeta().isDefaultValue).toBe(true)

    field.setValue([{ age: 0, name: '' }, null])
    expect(field.getMeta().isDefaultValue).toBe(false)

    field.setValue([{ age: 0 }, undefined])
    expect(field.getMeta().isDefaultValue).toBe(true)
  })

  it('should update the fields meta isDefaultValue with objects - simple', () => {
    const objectMetaForm = new FormApi({
      defaultValues: {
        obj: { firstName: 'John', lastName: 'Wick' },
      },
    })
    objectMetaForm.mount()

    const objectField = new FieldApi({
      form: objectMetaForm,
      name: 'obj',
    })
    objectField.mount()

    expect(objectField.getMeta().isDefaultValue).toBe(true)

    objectField.setValue({ firstName: 'John', lastName: 'Travolta' })
    expect(objectField.getMeta().isDefaultValue).toBe(false)

    objectField.setValue({ firstName: 'John', lastName: 'Wick' })
    expect(objectField.getMeta().isDefaultValue).toBe(true)
  })

  it('should update the fields meta isDefaultValue with objects - complex', () => {
    const defaultValues: { arr: [number, object]; test?: string } = {
      arr: [0, {}],
    }
    const form = new FormApi({
      defaultValues: {
        obj: defaultValues,
      },
    })
    form.mount()

    const field = new FieldApi({
      form: form,
      name: 'obj',
    })
    field.mount()

    expect(field.getMeta().isDefaultValue).toBe(true)

    field.setValue({
      arr: [1, {}],
      test: 'hi',
    })
    expect(field.getMeta().isDefaultValue).toBe(false)

    field.setValue({
      arr: [0, {}],
    })
    expect(field.getMeta().isDefaultValue).toBe(true)
  })

  it('should set isBlurred correctly', () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstName',
    })
    field.mount()

    expect(field.getMeta().isBlurred).toBe(false)

    field.setValue('Bob')
    expect(field.getMeta().isBlurred).toBe(false)

    field.handleBlur()
    expect(field.getMeta().isBlurred).toBe(true)
  })

  it('should set isBlurred correctly for arrays', () => {
    const form = new FormApi({
      defaultValues: {
        firstNames: ['Bob'],
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstNames',
    })
    field.mount()

    expect(field.getMeta().isBlurred).toBe(false)

    field.pushValue('Bill')
    expect(field.getMeta().isBlurred).toBe(false)

    field.handleBlur()
    expect(field.getMeta().isBlurred).toBe(true)
  })

  it('should push an array value correctly', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.pushValue('other')

    expect(field.getValue()).toStrictEqual(['one', 'other'])
  })

  it('should run onChange validation when pushing an array fields value', async () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
      validators: {
        onChange: ({ value }) => {
          if (value.length < 3) {
            return 'At least 3 names are required'
          }
          return
        },
      },
    })

    field.mount()

    field.pushValue('other')

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'At least 3 names are required',
    ])
  })

  it('should insert a value into an array value correctly', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.insertValue(1, 'other')

    expect(field.getValue()).toStrictEqual(['one', 'other', 'two'])
  })

  it('should replace a value into an array correctly', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.replaceValue(1, 'other')

    expect(field.getValue()).toStrictEqual(['one', 'other', 'three'])
  })

  it('should do nothing when replacing a value into an array at an index that does not exist', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.replaceValue(10, 'other')

    expect(field.getValue()).toStrictEqual(['one', 'two', 'three'])
  })

  it('should run onChange validation when inserting an array fields value', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
      validators: {
        onChange: ({ value }) => {
          if (value.length < 3) {
            return 'At least 3 names are required'
          }
          return
        },
      },
      defaultMeta: {
        isTouched: true,
      },
    })
    field.mount()

    field.insertValue(1, 'other')

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'At least 3 names are required',
    ])
  })

  it('should remove a value from an array value correctly', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.removeValue(1)

    expect(field.getValue()).toStrictEqual(['one'])
  })

  it('should run onChange validation when removing an array fields value', async () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
      validators: {
        onChange: ({ value }) => {
          if (value.length < 3) {
            return 'At least 3 names are required'
          }
          return
        },
      },
      defaultMeta: {
        isTouched: true,
      },
    })
    field.mount()

    await field.removeValue(0)

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'At least 3 names are required',
    ])
  })

  it('should remove a subfield from an array field correctly', async () => {
    const form = new FormApi({
      defaultValues: {
        people: [] as Array<{ name: string }>,
      },
    })

    const field = new FieldApi({
      form,
      name: 'people',
    })

    const subFieldValidators = {
      onChange: ({ value }: { value: string }) =>
        value.length === 0 ? 'Required' : null,
    }

    const subField1 = new FieldApi({
      form: field.form,
      name: 'people[0].name',
      defaultValue: '',
      validators: subFieldValidators,
    })

    const subField2 = new FieldApi({
      form: field.form,
      name: 'people[1].name',
      defaultValue: 'hello',
      validators: subFieldValidators,
    })

    const subField3 = new FieldApi({
      form: field.form,
      name: 'people[2].name',
      defaultValue: '',
      validators: subFieldValidators,
    })

    const subField4 = new FieldApi({
      form: field.form,
      name: 'people[3].name',
      defaultValue: 'world',
      validators: subFieldValidators,
    })

    form.mount()
    field.mount()
    subField1.mount()
    subField2.mount()
    subField3.mount()
    subField4.mount()

    await form.handleSubmit()

    expect(subField1.state.meta.errorMap.onChange).toStrictEqual('Required')
    expect(subField2.state.meta.errorMap.onChange).toStrictEqual(undefined)
    expect(subField3.state.meta.errorMap.onChange).toStrictEqual('Required')
    expect(subField4.state.meta.errorMap.onChange).toStrictEqual(undefined)

    await field.removeValue(0 /* subField1 */)

    expect(subField1.state.value).toBe('hello')
    expect(subField1.state.meta.errorMap.onChange).toStrictEqual(undefined)
    expect(subField2.state.value).toBe('')
    expect(subField2.state.meta.errorMap.onChange).toStrictEqual('Required')
    expect(subField3.state.value).toBe('world')
    expect(subField3.state.meta.errorMap.onChange).toStrictEqual(undefined)
    expect(form.getFieldInfo('people[0].name').instance?.state.value).toBe(
      'hello',
    )
    expect(form.getFieldInfo('people[1].name').instance?.state.value).toBe('')
    expect(form.getFieldInfo('people[2].name').instance?.state.value).toBe(
      'world',
    )
  })

  it('should remove remove the last subfield from an array field correctly', async () => {
    const form = new FormApi({
      defaultValues: {
        people: [{ name: '' }],
      },
    })

    const field = new FieldApi({
      form,
      name: 'people',
    })

    const subFieldValidators = {
      onChange: ({ value }: { value: string }) =>
        value.length === 0 ? 'Required' : null,
    }

    const subField1 = new FieldApi({
      form: field.form,
      name: 'people[0].name',
      validators: subFieldValidators,
    })

    form.mount()
    field.mount()
    subField1.mount()

    await form.handleSubmit()

    expect(subField1.state.meta.errorMap.onChange).toStrictEqual('Required')

    await field.removeValue(0 /* subField1 */)

    expect(subField1.state.value).toBe(undefined)
    expect(subField1.state.meta.isValid).toBe(true)
    expect(subField1.state.meta.errorMap.onChange).toStrictEqual(undefined)

    expect(form.state.isFieldsValid).toBe(true)
    expect(form.state.canSubmit).toBe(true)
  })

  it('should swap a value from an array value correctly', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.swapValues(0, 1)

    expect(field.getValue()).toStrictEqual(['two', 'one'])
  })

  it('should run onChange validation when swapping an array fields value', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2'],
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
      validators: {
        onChange: ({ value }) => {
          if (value.length < 3) {
            return 'At least 3 names are required'
          }
          return
        },
      },
      defaultMeta: {
        isTouched: true,
      },
    })
    field.mount()

    field.swapValues(0, 1)

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'At least 3 names are required',
    ])
  })

  it('should move a value from an array value correctly', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three', 'four'],
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()

    field.moveValue(2, 0)

    expect(field.getValue()).toStrictEqual(['three', 'one', 'two', 'four'])
  })

  it('should run onChange validation when moving an array fields value', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2'],
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'names',
      validators: {
        onChange: ({ value }) => {
          if (value.length < 3) {
            return 'At least 3 names are required'
          }
          return
        },
      },
      defaultMeta: {
        isTouched: true,
      },
    })
    field.mount()

    field.moveValue(0, 1)

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'At least 3 names are required',
    ])
  })

  it('should add a field when the key is numeric but the parent is not an array', () => {
    const form = new FormApi({
      defaultValues: {
        items: {} as Record<number, { quantity: number }>,
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'items.2.quantity',
    })

    field.setValue(10)

    expect(form.state.values).toStrictEqual({
      items: {
        2: {
          quantity: 10,
        },
      },
    })
  })

  it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })

    field.mount()

    expect(() =>
      form.update({
        defaultValues: {
          name: 'other',
        },
      }),
    ).not.toThrow()
  })

  it('should run validation onChange', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChange: ({ value }) => {
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)

    field.setValue('other')

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })

    field.setValue('nothing')

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
  })

  it('should run async validation onChange', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChangeAsync: async ({ value }) => {
          await sleep(1000)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    await vi.runAllTimersAsync()
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should run async validation onChange with debounce', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChangeAsyncDebounceMs: 1000,
        onChangeAsync: async ({ value }) => {
          await sleepMock(1000)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    field.setValue('other', {
      dontUpdateMeta: true,
    })
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without onChangeAsyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should run async validation onChange with asyncDebounceMs', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      asyncDebounceMs: 1000,
      validators: {
        onChangeAsync: async ({ value }) => {
          await sleepMock(1000)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    field.setValue('other', {
      dontUpdateMeta: true,
    })
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without asyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should abort enqueued debounced async validation if sync validation fails in the meantime', async () => {
    vi.useFakeTimers()

    const mockOnChange = vi.fn().mockImplementation(({ value }) => {
      if (value.length < 3) {
        return 'First name must be at least 3 characters'
      }
      return
    })

    const mockOnChangeAsync = vi.fn().mockImplementation(async ({ value }) => {
      return value.includes('error') && 'No "error" allowed in first name'
    })

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChange: mockOnChange,
        onChangeAsyncDebounceMs: 500,
        onChangeAsync: mockOnChangeAsync,
      },
    })

    field.mount()

    field.setValue('123')
    expect(mockOnChange).toHaveBeenCalledTimes(1)
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(0)
    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors).toStrictEqual([])

    // Change value while debounced async validation is enqueued
    field.setValue('12')
    expect(mockOnChange).toHaveBeenCalledTimes(2)
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(0)

    // Async validation never got called because sync validation failed in the meantime and aborted the async
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(0)
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'First name must be at least 3 characters',
    ])
  })

  it("should not remove sync validation errors when async validation doesn't return an error", async () => {
    vi.useFakeTimers()

    const mockOnChange = vi.fn().mockImplementation(({ value }) => {
      if (value.length < 3) {
        return 'First name must be at least 3 characters'
      }
      return
    })

    const mockOnChangeAsync = vi.fn().mockImplementation(async ({ value }) => {
      await sleep(1000)
      return value.includes('error') && 'No "error" allowed in first name'
    })

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChange: mockOnChange,
        onChangeAsyncDebounceMs: 500,
        onChangeAsync: mockOnChangeAsync,
      },
    })

    field.mount()

    // Input a valid value, triggers both validations after debounce + sleep
    field.setValue('1234')
    expect(mockOnChange).toHaveBeenCalledTimes(1)
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(0)
    await vi.runAllTimersAsync()
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(1)
    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors).toStrictEqual([])

    // Input again a valid value
    field.setValue('123')
    expect(mockOnChange).toHaveBeenCalledTimes(2)
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(1)
    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors).toStrictEqual([])

    // Wait the debounce time, async validation is called
    await vi.advanceTimersByTimeAsync(500)
    expect(mockOnChangeAsync).toHaveBeenCalledTimes(2)

    // Input an invalid value before async validation resolves
    field.setValue('12')
    expect(mockOnChange).toHaveBeenCalledTimes(3)
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'First name must be at least 3 characters',
    ])

    // Wait for async validation to resolve
    await vi.runAllTimersAsync()
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'First name must be at least 3 characters',
    ])
  })

  it('should run validation onBlur', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onBlur: ({ value }) => {
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    field.setValue('other')
    field.validate('blur')
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onBlur', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onBlurAsync: async ({ value }) => {
          await sleep(1000)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()
    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    field.validate('blur')
    await vi.runAllTimersAsync()
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onBlur with debounce', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onBlurAsyncDebounceMs: 1000,
        onBlurAsync: async ({ value }) => {
          await sleepMock(10)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    field.validate('blur')
    field.validate('blur')
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without onBlurAsyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onBlur with asyncDebounceMs', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      asyncDebounceMs: 1000,
      validators: {
        onBlurAsync: async ({ value }) => {
          await sleepMock(10)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    field.validate('blur')
    field.validate('blur')
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without asyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onSubmit', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onSubmitAsync: async ({ value }) => {
          await sleep(1000)
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors.length).toBe(0)
    field.setValue('other')
    field.validate('submit')
    await vi.runAllTimersAsync()
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toContain('Please enter a different value')
    expect(field.getMeta().errorMap).toMatchObject({
      onSubmit: 'Please enter a different value',
    })
  })

  it('should run listener onChange', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    let triggered!: string
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onChange: ({ value }) => {
          triggered = value
        },
      },
    })

    field.mount()

    field.setValue('other')
    expect(triggered).toStrictEqual('other')
  })

  it('should not run the listener onChange on mount', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    let triggered!: string
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onChange: ({ value }) => {
          triggered = value
        },
      },
    })

    field.mount()

    expect(triggered).toStrictEqual(undefined)
  })

  it('should change the form state when running listener onChange', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'foo',
        greet: 'bar',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onChange: ({ value }) => {
          form.setFieldValue('greet', `hello ${value}`)
        },
      },
    })

    field.mount()

    field.setValue('baz')
    expect(form.getFieldValue('name')).toStrictEqual('baz')
    expect(form.getFieldValue('greet')).toStrictEqual('hello baz')
  })

  it('should run the onChange listener when the field array is changed', () => {
    const form = new FormApi({
      defaultValues: {
        items: ['one', 'two'],
      },
    })
    form.mount()

    let arr!: string[]

    const field = new FieldApi({
      form,
      name: 'items',
      listeners: {
        onChange: ({ value }) => {
          arr = value
        },
      },
    })
    field.mount()

    field.removeValue(1)
    expect(arr).toStrictEqual(['one'])

    field.replaceValue(0, 'start')
    expect(arr).toStrictEqual(['start'])

    field.pushValue('end')
    expect(arr).toStrictEqual(['start', 'end'])

    field.insertValue(1, 'middle')
    expect(arr).toStrictEqual(['start', 'middle', 'end'])

    field.swapValues(0, 2)
    expect(arr).toStrictEqual(['end', 'middle', 'start'])

    field.moveValue(0, 1)
    expect(arr).toStrictEqual(['middle', 'end', 'start'])

    field.clearValues()
    expect(arr).toStrictEqual([])
  })

  it('should not break when clearValues is called on a non-array field', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'foo',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })

    field.mount()

    expect(() => field.clearValues()).not.toThrow()
  })

  it('should reset the form on a listener', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'foo',
        greet: 'bar',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onChange: () => {
          form.reset({
            ...form.state.values,
            greet: '',
          })
        },
      },
    })

    field.mount()

    field.setValue('other')
    expect(form.getFieldValue('name')).toStrictEqual('other')
    expect(form.getFieldValue('greet')).toStrictEqual('')
  })

  it('should run listener onBlur', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    let triggered!: string
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onBlur: ({ value }) => {
          triggered = value
        },
      },
    })

    field.mount()

    field.handleBlur()
    expect(triggered).toStrictEqual('test')
  })

  it('should run listener onMount', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    let triggered!: string
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onMount: ({ value }) => {
          triggered = value
        },
      },
    })

    field.mount()

    expect(triggered).toStrictEqual('test')
  })

  it('should contain multiple errors when running validation onBlur and onChange', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onBlur: ({ value }) => {
          if (value === 'other') return 'Please enter a different value'
          return
        },
        onChange: ({ value }) => {
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    field.setValue('other')
    field.validate('blur')
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'Please enter a different value',
      'Please enter a different value',
    ])
    expect(field.getMeta().errorMap).toEqual({
      onBlur: 'Please enter a different value',
      onChange: 'Please enter a different value',
    })
  })

  it('should reset onChange errors when the issue is resolved', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChange: ({ value }) => {
          if (value === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    field.mount()

    field.setValue('other')
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual([
      'Please enter a different value',
    ])
    expect(field.getMeta().errorMap).toEqual({
      onChange: 'Please enter a different value',
    })
    field.setValue('test')
    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors).toStrictEqual([])
    expect(field.getMeta().errorMap).toEqual({})
  })

  it('should handle default value on field using state.value', async () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      defaultValue: 'test',
    })

    field.mount()

    expect(field.state.value).toBe('test')
  })

  // test the unmounting of the fieldAPI
  it('should preserve value on unmount', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })

    const unmount = field.mount()
    unmount()
    expect(form.getFieldInfo(field.name).instance).toBeDefined()
    expect(form.getFieldInfo(field.name)).toBeDefined()
  })

  it('should show onSubmit errors', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onSubmit: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
    })

    field.mount()

    await form.handleSubmit()
    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual(['first name is required'])
  })

  it('should show onMount errors', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onMount: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
    })

    form.mount()
    field.mount()

    expect(field.getMeta().isValid).toBe(false)
    expect(field.getMeta().errors).toStrictEqual(['first name is required'])
  })

  it('should disable submit with onMount errors', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onMount: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
    })

    form.mount()
    field.mount()

    expect(form.state.canSubmit).toBe(false)
  })

  it('should remove onMount errors on a field when its value changes', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })

    const firstName = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onMount: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
        onChange: ({ value }) =>
          value.length > 3 ? undefined : 'first name must be at least 4 chars',
      },
    })

    const lastName = new FieldApi({
      form,
      name: 'lastName',
      validators: {
        onMount: ({ value }) =>
          value.length > 0 ? undefined : 'last name is required',
        onChange: ({ value }) =>
          value.length > 3 ? undefined : 'last name must be at least 4 chars',
      },
    })

    form.mount()
    firstName.mount()
    lastName.mount()

    expect(firstName.getMeta().isValid).toBe(false)
    expect(firstName.getMeta().errorMap.onMount).toStrictEqual(
      'first name is required',
    )
    expect(firstName.getMeta().errors).toStrictEqual(['first name is required'])

    expect(lastName.getMeta().isValid).toBe(false)
    expect(lastName.getMeta().errors).toStrictEqual(['last name is required'])
    expect(lastName.getMeta().errorMap.onMount).toStrictEqual(
      'last name is required',
    )

    firstName.setValue('firstName')
    expect(firstName.getMeta().isValid).toBe(true)
    expect(firstName.getMeta().errors).toStrictEqual([])
    expect(firstName.getMeta().errorMap.onMount).toStrictEqual(undefined)

    expect(lastName.getMeta().isValid).toBe(false)
    expect(lastName.getMeta().errors).toStrictEqual(['last name is required'])
    expect(lastName.getMeta().errorMap.onMount).toStrictEqual(
      'last name is required',
    )

    firstName.setValue('f')
    expect(firstName.getMeta().isValid).toBe(false)
    expect(firstName.getMeta().errors).toStrictEqual([
      'first name must be at least 4 chars',
    ])
    expect(firstName.getMeta().errorMap.onMount).toStrictEqual(undefined)
    expect(firstName.getMeta().errorMap.onChange).toStrictEqual(
      'first name must be at least 4 chars',
    )
  })

  it('should cancel previous functions from an async validator with an abort signal', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })

    form.mount()

    let resolve!: () => void
    const promise = new Promise((r) => {
      resolve = r as never
    })

    const fn = vi.fn()

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChangeAsyncDebounceMs: 0,
        onChangeAsync: async ({ signal }) => {
          await promise
          if (signal.aborted) return
          fn()
          return undefined
        },
      },
    })

    field.mount()

    field.setValue('one')
    await vi.runAllTimersAsync()
    field.setValue('two')
    resolve()
    await vi.runAllTimersAsync()
    expect(fn).toHaveBeenCalledTimes(1)
  })

  it('should run onChange on a linked field', () => {
    const form = new FormApi({
      defaultValues: {
        password: '',
        confirm_password: '',
      },
    })

    form.mount()

    const passField = new FieldApi({
      form,
      name: 'password',
    })

    const passconfirmField = new FieldApi({
      form,
      name: 'confirm_password',
      validators: {
        onChangeListenTo: ['password'],
        onChange: ({ value, fieldApi }) => {
          if (value !== fieldApi.form.getFieldValue('password')) {
            return 'Passwords do not match'
          }
          return undefined
        },
      },
    })

    passField.mount()
    passconfirmField.mount()

    passField.setValue('one')
    expect(passconfirmField.getMeta().isValid).toBe(false)
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
    passconfirmField.setValue('one')
    expect(passconfirmField.getMeta().isValid).toBe(true)
    expect(passconfirmField.state.meta.errors).toStrictEqual([])
    passField.setValue('two')
    expect(passconfirmField.getMeta().isValid).toBe(false)
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
  })

  it('should run onBlur on a linked field', () => {
    const form = new FormApi({
      defaultValues: {
        password: '',
        confirm_password: '',
      },
    })

    form.mount()

    const passField = new FieldApi({
      form,
      name: 'password',
    })

    const passconfirmField = new FieldApi({
      form,
      name: 'confirm_password',
      validators: {
        onBlurListenTo: ['password'],
        onBlur: ({ value, fieldApi }) => {
          if (value !== fieldApi.form.getFieldValue('password')) {
            return 'Passwords do not match'
          }
          return undefined
        },
      },
    })

    passField.mount()
    passconfirmField.mount()

    passField.setValue('one')
    expect(passconfirmField.state.meta.errors).toStrictEqual([])
    passField.handleBlur()
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
    passconfirmField.setValue('one')
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
    passField.handleBlur()
    expect(passconfirmField.state.meta.errors).toStrictEqual([])
    passField.setValue('two')
    passField.handleBlur()
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
  })

  it('should run onChangeAsync on a linked field', async () => {
    vi.useFakeTimers()
    let resolve!: () => void
    let promise = new Promise((r) => {
      resolve = r as never
    })

    const fn = vi.fn()

    const form = new FormApi({
      defaultValues: {
        password: '',
        confirm_password: '',
      },
    })

    form.mount()

    const passField = new FieldApi({
      form,
      name: 'password',
    })

    const passconfirmField = new FieldApi({
      form,
      name: 'confirm_password',
      validators: {
        onChangeListenTo: ['password'],
        onChangeAsync: async ({ value, fieldApi }) => {
          await promise
          fn()
          if (value !== fieldApi.form.getFieldValue('password')) {
            return 'Passwords do not match'
          }
          return undefined
        },
      },
    })

    passField.mount()
    passconfirmField.mount()

    passField.setValue('one')
    resolve()
    await vi.runAllTimersAsync()
    expect(passconfirmField.getMeta().isValid).toBe(false)
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
    promise = new Promise((r) => {
      resolve = r as never
    })
    passconfirmField.setValue('one')
    resolve()
    await vi.runAllTimersAsync()
    expect(passconfirmField.getMeta().isValid).toBe(true)
    expect(passconfirmField.state.meta.errors).toStrictEqual([])
    promise = new Promise((r) => {
      resolve = r as never
    })
    passField.setValue('two')
    resolve()
    await vi.runAllTimersAsync()
    expect(passconfirmField.getMeta().isValid).toBe(false)
    expect(passconfirmField.state.meta.errors).toStrictEqual([
      'Passwords do not match',
    ])
  })

  it('should add  a new value to the fieldApi errorMap', () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })
    form.mount()
    const nameField = new FieldApi({
      form,
      name: 'name',
    })
    nameField.mount()
    nameField.setErrorMap({ onChange: "name can't be Josh" as never })
    expect(nameField.getMeta().isValid).toBe(false)
    expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh")
  })
  it('should preserve other values in the fieldApi errorMap when adding other values', () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })
    form.mount()
    const nameField = new FieldApi({
      form,
      name: 'name',
    })
    nameField.mount()
    nameField.setErrorMap({ onChange: "name can't be Josh" as never })
    expect(nameField.getMeta().isValid).toBe(false)
    expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh")
    nameField.setErrorMap({ onBlur: 'name must begin with uppercase' as never })
    expect(nameField.getMeta().isValid).toBe(false)
    expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh")
    expect(nameField.getMeta().errorMap.onBlur).toEqual(
      'name must begin with uppercase',
    )
  })
  it('should replace errorMap value if it exists in the fieldApi object', () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })
    form.mount()
    const nameField = new FieldApi({
      form,
      name: 'name',
    })
    nameField.mount()
    nameField.setErrorMap({ onChange: "name can't be Josh" as never })
    expect(nameField.getMeta().isValid).toBe(false)
    expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh")
    nameField.setErrorMap({ onChange: 'other validation error' as never })
    expect(nameField.getMeta().errorMap.onChange).toEqual(
      'other validation error',
    )
  })

  it('should have derived state on first render given defaultMeta', () => {
    const form = new FormApi({
      defaultValues: {
        name: '',
      },
    })
    form.mount()

    const nameField = new FieldApi({
      form,
      name: 'name',
      defaultMeta: {
        errorMap: {
          onChange: 'THERE IS AN ERROR',
        } as never,
      },
    })

    nameField.mount()
    expect(nameField.getMeta().errors).toEqual(['THERE IS AN ERROR'])
  })

  it('should remove the meta from deleted fields', async () => {
    const form = new FormApi({
      defaultValues: {
        names: [
          {
            firstName: 'John',
            lastName: '',
          },
          {
            firstName: 'Martha',
            lastName: 'Mustermann',
          },
        ],
      },
    })

    const field = new FieldApi({
      form,
      name: `names[${0}].lastName`,
      validators: {
        onMount: ({ value }) =>
          value.length > 0 ? undefined : 'Last name is required',
        onChange: ({ value }) =>
          value.length > 0 ? undefined : 'Last name is required',
      },
    })

    form.mount()
    field.mount()

    expect(form.state.canSubmit).toBe(false)

    await form.removeFieldValue('names', 0)

    expect(form.getFieldValue('names')).toEqual([
      {
        firstName: 'Martha',
        lastName: 'Mustermann',
      },
    ])

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errors).toStrictEqual([])
    expect(form.state.canSubmit).toBe(true)
  })

  it('should remove the meta from deleted object fields', async () => {
    const form = new FormApi({
      defaultValues: {
        names: {
          john: {
            firstName: 'John',
            lastName: '',
          },
          martha: {
            firstName: 'Martha',
            lastName: 'Mustermann',
          },
        },
      },
    })

    const field = new FieldApi({
      form,
      name: 'names.john.lastName',
      validators: {
        onChange: () => 'Last name is required',
      },
    })

    form.mount()
    field.mount()

    field.setValue('JohnLastName')

    expect(form.state.canSubmit).toBe(false)

    form.deleteField('names.john')

    expect(form.getFieldValue('names')).toEqual({
      martha: {
        firstName: 'Martha',
        lastName: 'Mustermann',
      },
    })

    expect(field.getMeta().isValid).toBe(true)
    expect(field.getMeta().errorMap).toStrictEqual({})
    expect(field.getMeta().errors).toStrictEqual([])
    expect(form.state.canSubmit).toBe(true)
  })

  it('should debounce onChange listener', async () => {
    vi.useFakeTimers()
    const form = new FormApi({
      defaultValues: {
        name: '',
      },
    })

    form.mount()

    const onChangeMock = vi.fn()
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onChange: onChangeMock,
        onChangeDebounceMs: 500,
      },
    })

    field.mount()

    field.handleChange('first')
    field.handleChange('second')
    expect(onChangeMock).not.toHaveBeenCalled()

    await vi.advanceTimersByTimeAsync(500)
    expect(onChangeMock).toHaveBeenCalledTimes(1)
    expect(onChangeMock).toHaveBeenCalledWith({
      value: 'second',
      fieldApi: field,
    })
  })

  it('should debounce onBlur listener', async () => {
    vi.useFakeTimers()
    const form = new FormApi({
      defaultValues: {
        name: '',
      },
    })

    form.mount()

    const onBlurMock = vi.fn()
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onBlur: onBlurMock,
        onBlurDebounceMs: 300,
      },
    })

    field.mount()

    field.handleBlur()
    field.handleBlur()
    expect(onBlurMock).not.toHaveBeenCalled()

    await vi.advanceTimersByTimeAsync(300)
    expect(onBlurMock).toHaveBeenCalledTimes(1)
  })

  it('should pass the current value to the Standard Schema when calling parseValueWithSchema', async () => {
    const schema = z.string().min(3)

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstName',
    })
    field.mount()

    // The schema should complain that the value is too short
    const issueResult = field.parseValueWithSchema(schema)
    expect(issueResult).toBeDefined()
    expect(Array.isArray(issueResult)).toBe(true)
    expect(issueResult?.length).toBeGreaterThan(0)

    field.setValue('some long name that satisfies firstNameSchemaResult')
    // the schema should now be satisfied
    const successResult = field.parseValueWithSchema(schema)
    expect(successResult).toBeUndefined()
  })

  it('should pass the current value to the Standard Schema when calling parseValueWithSchemaAsync', async () => {
    const schema = z.string().min(3)

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstName',
    })
    field.mount()

    // The schema should complain that the value is too short
    const issuePromise = field.parseValueWithSchemaAsync(schema)
    expect(issuePromise).toBeInstanceOf(Promise)

    const issueResult = await issuePromise

    expect(issueResult).toBeDefined()
    expect(Array.isArray(issueResult)).toBe(true)
    expect(issueResult?.length).toBeGreaterThan(0)

    field.setValue('some long name that satisfies firstNameSchemaResult')
    // the schema should now be satisfied
    const successPromise = field.parseValueWithSchemaAsync(schema)
    expect(successPromise).toBeInstanceOf(Promise)

    const successResult = await successPromise
    expect(successResult).toBeUndefined()
  })

  it('should throw an error when passing an async Standard Schema to parseValueWithSchema', async () => {
    const testSchema = z.string().superRefine(async () => {
      await sleep(1000)
      return true
    })

    const form = new FormApi({
      defaultValues: {
        name: '',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })
    field.mount()

    // async passed to sync should error
    expect(() => {
      field.parseValueWithSchema(testSchema)
    }).toThrowError()
    // async to async is fine
    expect(() => {
      field.parseValueWithSchemaAsync(testSchema)
    }).not.toThrowError()
    // sync to async is also fine
    expect(() => {
      field.parseValueWithSchemaAsync(z.any())
    }).not.toThrowError()
  })

  it('should update submission meta when calling handleSubmit', async () => {
    let doError = true
    let externalAttemptsCounter = 0
    const form = new FormApi({
      defaultValues: {
        firstName: 'John',
        lastName: 'Doe',
      },
      validators: {
        onChange: () => (doError ? 'form error' : undefined),
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })
    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })

    form.mount()
    firstNameField.mount()
    lastNameField.mount()

    // assert initial state
    expect(form.state.canSubmit).toBe(true)
    expect(form.state.isFormValid).toBe(true)
    expect(form.state.isSubmitted).toBe(false)
    expect(form.state.isSubmitSuccessful).toBe(false)
    expect(form.state.submissionAttempts).toBe(externalAttemptsCounter)

    await form.handleSubmit()
    externalAttemptsCounter += 1

    // error occurred. The meta should reflect it
    expect(form.state.canSubmit).toBe(false)
    expect(form.state.isFormValid).toBe(false)
    expect(form.state.isSubmitted).toBe(false)
    expect(form.state.isSubmitSuccessful).toBe(false)
    expect(form.state.submissionAttempts).toBe(externalAttemptsCounter)

    await form.handleSubmit()
    externalAttemptsCounter += 1

    // form has errors and was still submitted. Meta should reflect that
    expect(form.state.canSubmit).toBe(false)
    expect(form.state.isFormValid).toBe(false)
    expect(form.state.isSubmitted).toBe(false)
    expect(form.state.isSubmitSuccessful).toBe(false)
    expect(form.state.submissionAttempts).toBe(externalAttemptsCounter)

    // remove error and touched state
    doError = false
    form.validate('change')

    await form.handleSubmit()
    externalAttemptsCounter += 1

    // form had no errors, assert that it was successful
    expect(form.state.canSubmit).toBe(true)
    expect(form.state.isFormValid).toBe(true)
    expect(form.state.isSubmitted).toBe(true)
    expect(form.state.isSubmitSuccessful).toBe(true)
    expect(form.state.submissionAttempts).toBe(externalAttemptsCounter)
  })

  it('should touch all fields when calling handleSubmit regardless of errors', async () => {
    let doError = true
    const form = new FormApi({
      defaultValues: {
        firstName: 'John',
        lastName: 'Doe',
      },
      validators: {
        onChange: () => (doError ? 'form error' : undefined),
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })
    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })

    form.mount()
    firstNameField.mount()
    lastNameField.mount()

    function undoTouchedState() {
      firstNameField.setMeta((prev) => ({ ...prev, isTouched: false }))
      lastNameField.setMeta((prev) => ({ ...prev, isTouched: false }))
    }

    // assert initial state
    expect(firstNameField.getMeta().isTouched).toBe(false)
    expect(lastNameField.getMeta().isTouched).toBe(false)

    await form.handleSubmit()

    // form has errors now. All fields should be touched
    expect(firstNameField.getMeta().isTouched).toBe(true)
    expect(lastNameField.getMeta().isTouched).toBe(true)

    undoTouchedState()
    await form.handleSubmit()

    // form has errors and was still submitted. Fields should still be touched
    expect(firstNameField.getMeta().isTouched).toBe(true)
    expect(lastNameField.getMeta().isTouched).toBe(true)

    // remove error
    doError = false
    form.validate('change')

    undoTouchedState()
    await form.handleSubmit()

    // submission succeeded. Fields should be touched
    expect(firstNameField.getMeta().isTouched).toBe(true)
    expect(lastNameField.getMeta().isTouched).toBe(true)
  })

  it('should not trigger form validators in handleSubmit if field validators errored', async () => {
    const validators = {
      onChange: () => 'onChange',
      onBlur: () => 'onBlur',
      onSubmit: () => 'onSubmit',
    }
    const allErrors = ['onChange', 'onBlur', 'onSubmit']
    const form = new FormApi({
      defaultValues: {
        firstName: 'John',
        lastName: 'Doe',
      },
      validators,
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
      validators,
    })
    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
      validators,
    })

    form.mount()
    firstNameField.mount()
    lastNameField.mount()

    // since we have no mount error, we expect all errors to be empty
    expect(form.state.canSubmit).toBe(true)
    expect(form.getAllErrors().form.errors).toEqual([])
    expect(firstNameField.getMeta().errors).toEqual([])
    expect(lastNameField.getMeta().errors).toEqual([])

    await form.handleSubmit()

    // after handling submission, the form should reflect the changes
    expect(form.state.canSubmit).toBe(false)
    expect(form.getAllErrors().form.errors).toEqual([])
    expect(firstNameField.getMeta().errors).toEqual(allErrors)
    expect(lastNameField.getMeta().errors).toEqual(allErrors)
  })

  it('should trigger form validators in handleSubmit if field validators are valid', async () => {
    const validators = {
      onChange: () => 'onChange',
      onBlur: () => 'onBlur',
      onSubmit: () => 'onSubmit',
    }
    const allErrors = ['onChange', 'onBlur', 'onSubmit']
    const form = new FormApi({
      defaultValues: {
        firstName: 'John',
        lastName: 'Doe',
      },
      validators,
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })
    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })

    form.mount()
    firstNameField.mount()
    lastNameField.mount()

    // since we have no mount error, we expect all errors to be empty
    expect(form.state.canSubmit).toBe(true)
    expect(form.getAllErrors().form.errors).toEqual([])
    expect(firstNameField.getMeta().errors).toEqual([])
    expect(lastNameField.getMeta().errors).toEqual([])

    await form.handleSubmit()

    // field validators passed, so form validators should've triggered
    expect(form.state.canSubmit).toBe(false)
    expect(form.getAllErrors().form.errors).toEqual(allErrors)
    expect(firstNameField.getMeta().errors).toEqual([])
    expect(lastNameField.getMeta().errors).toEqual([])
  })

  it('should update the errorSourceMap with field source when field async field error is added', async () => {
    vi.useFakeTimers()
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChangeAsync: async () => {
          return 'Error'
        },
      },
    })
    field.mount()

    field.setValue('test')
    await vi.runAllTimersAsync()

    expect(field.getMeta().errorSourceMap.onChange).toEqual('field')
  })

  it('should not run onChange validation when onBlur is triggered', () => {
    const form = new FormApi({
      defaultValues: { a: '' },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'a',
      validators: {
        onChange: () => 'Change error',
        onBlur: () => 'Blur error',
      },
    })
    field.mount()

    field.handleBlur()

    expect(field.state.meta.errors).toStrictEqual(['Blur error'])
  })

  it('should allow setting to explicitly undefined', () => {
    const form = new FormApi({
      defaultValues: { a: '' as string | undefined },
    })
    form.mount()

    const field = new FieldApi({ form, name: 'a' })
    field.mount()

    expect(field.state.value).toBe('')
    field.handleChange(undefined)
    expect(field.state.value).toBeUndefined()
  })
})


--- packages/form-core/tests/FieldApi.test-d.ts ---
import { expectTypeOf, it } from 'vitest'
import { z } from 'zod'
import { FieldApi, FormApi } from '../src/index'
import type { StandardSchemaV1Issue } from '../src/index'

it('should type value properly', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  } as const)

  const field = new FieldApi({
    form,
    name: 'name',
  })

  expectTypeOf(field.state.value).toEqualTypeOf<'test'>()
  expectTypeOf(field.options.name).toEqualTypeOf<'name'>()
  expectTypeOf(field.getValue()).toEqualTypeOf<'test'>()
})

it('should type value when nothing is passed into constructor', () => {
  type FormValues = {
    name?: string
    age?: number
  }

  const form = new FormApi({ defaultValues: {} as FormValues })

  const field = new FieldApi({
    form,
    name: 'name' as const,
  })

  expectTypeOf(field.state.value).toEqualTypeOf<string | undefined>()
  expectTypeOf(field.options.name).toEqualTypeOf<'name'>()
  expectTypeOf(field.getValue()).toEqualTypeOf<string | undefined>()
})

it('should type required fields in constructor', () => {
  type FormValues = {
    name: string
    age?: number
  }

  const form = new FormApi({
    defaultValues: {
      name: 'test',
    } as FormValues,
  })

  const field = new FieldApi({
    form,
    name: 'name' as const,
  })

  expectTypeOf(field.state.value).toEqualTypeOf<string>()
  expectTypeOf(field.options.name).toEqualTypeOf<'name'>()
  expectTypeOf(field.getValue()).toEqualTypeOf<string>()
})

it('should type value properly for completely partial forms', () => {
  type CompletelyPartialFormValues = {
    name?: 'test'
    age?: number
  }

  const form = new FormApi({
    defaultValues: {} as CompletelyPartialFormValues,
  })

  const field = new FieldApi({
    form,
    name: 'name' as const,
  })

  expectTypeOf(field.state.value).toEqualTypeOf<'test' | undefined>()
  expectTypeOf(field.options.name).toEqualTypeOf<'name'>()
  expectTypeOf(field.getValue()).toEqualTypeOf<'test' | undefined>()
})

it('should type onChange properly', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  } as const)

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: ({ value }) => {
        expectTypeOf(value).toEqualTypeOf<'test'>()

        return undefined
      },
    },
  })
})

it('should type onChangeAsync properly', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  } as const)

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChangeAsync: async ({ value }) => {
        expectTypeOf(value).toEqualTypeOf<'test'>()

        return undefined
      },
    },
  })
})

it('should type an array sub-field properly', () => {
  type Person = {
    name: string
    age: number
  }

  const form = new FormApi({
    defaultValues: {
      nested: {
        people: [] as Person[],
      },
    },
  } as const)

  const field = new FieldApi({
    form,
    name: `nested.people[1].name`,
    validators: {
      onChangeAsync: async ({ value }) => {
        expectTypeOf(value).toEqualTypeOf<string>()

        return undefined
      },
    },
  })

  expectTypeOf(field.state.value).toEqualTypeOf<string>()
})

it('should have the correct types returned from form validators', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
    validators: {
      onChange: () => {
        return '123' as const
      },
    },
  } as const)

  expectTypeOf(form.state.errorMap.onChange).toEqualTypeOf<'123' | undefined>()
})

it('should have the correct types returned from form validators even when both onChange and onChangeAsync are present', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
    validators: {
      onChange: () => {
        return '123' as const
      },
      onChangeAsync: async () => {
        return '123' as const
      },
    },
  } as const)

  expectTypeOf(form.state.errorMap.onChange).toEqualTypeOf<'123' | undefined>()
})

it('should have the correct types returned from field validators', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  } as const)

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: () => {
        return '123' as const
      },
    },
  })

  expectTypeOf(field.state.meta.errorMap.onChange).toEqualTypeOf<
    '123' | undefined
  >()
})

it('should have the correct types returned from field validators in array', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  } as const)

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: () => {
        return '123' as const
      },
    },
  })

  expectTypeOf(field.state.meta.errors).toEqualTypeOf<
    Array<'123' | undefined>
  >()
})

it('should have the correct types returned from form validators in array', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
    validators: {
      onChange: () => {
        return '123' as const
      },
    },
  } as const)

  expectTypeOf(form.state.errors).toEqualTypeOf<Array<'123' | undefined>>()
})

it('should handle "fields" return types added to the field\'s errorMap itself', () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
    },
    validators: {
      onChange: () => {
        return {
          fields: {
            firstName: 'Testing' as const,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'firstName',
  })

  expectTypeOf(field.getMeta().errorMap.onChange).toEqualTypeOf<
    'Testing' | undefined
  >()
})

it('should handle "fields" return types added to the field\'s error array itself', () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
    },
    validators: {
      onChange: () => {
        return {
          fields: {
            firstName: 'Testing' as const,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'firstName',
  })

  expectTypeOf(field.getMeta().errors).toEqualTypeOf<
    Array<'Testing' | undefined>
  >()
})

it('should handle "fields" async return types added to the field\'s errorMap itself', () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
    },
    validators: {
      onChangeAsync: async () => {
        return {
          fields: {
            firstName: 'Testing' as const,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'firstName',
  })

  expectTypeOf(field.getMeta().errorMap.onChange).toEqualTypeOf<
    'Testing' | undefined
  >()
})

it('should handle "fields" async return types added to the field\'s error array itself', () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
    },
    validators: {
      onChangeAsync: async () => {
        return {
          fields: {
            firstName: 'Testing' as const,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'firstName',
  })

  expectTypeOf(field.getMeta().errors).toEqualTypeOf<
    Array<'Testing' | undefined>
  >()
})

it('should handle "sub-fields" async return types added to the field\'s error array itself', () => {
  const form = new FormApi({
    defaultValues: {
      person: {
        firstName: '',
      },
    },
    validators: {
      onChangeAsync: async () => {
        return {
          fields: {
            'person.firstName': 'Testing' as const,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'person.firstName',
  })

  expectTypeOf(field.getMeta().errors).toEqualTypeOf<
    Array<'Testing' | undefined>
  >()
})

it('should only have field-level error types returned from parseValueWithSchema and parseValueWithSchemaAsync', () => {
  const form = new FormApi({
    defaultValues: { name: '' },
  })
  form.mount()

  const field = new FieldApi({
    form,
    name: 'name',
  })
  field.mount()

  const schema = z.string()
  // assert that it doesn't think it's a form-level error
  expectTypeOf(field.parseValueWithSchema(schema)).toEqualTypeOf<
    StandardSchemaV1Issue[] | undefined
  >()
  expectTypeOf(field.parseValueWithSchemaAsync(schema)).toEqualTypeOf<
    Promise<StandardSchemaV1Issue[] | undefined>
  >()
})

it("should allow setting manual errors according to the validator's return type", () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    validators: {
      onChange: () => {
        return {
          fields: {
            firstName: '123' as const,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'firstName',
    validators: {
      onChange: () => 10 as const,
      onBlur: () => ['onBlur'] as const,
    },
  })

  field.setErrorMap({
    onChange: '123',
  })

  expectTypeOf(field.setErrorMap).parameter(0).toEqualTypeOf<{
    onMount: undefined
    onChange: '123' | 10 | undefined
    onBlur: readonly ['onBlur'] | undefined
    onSubmit: undefined
    onServer: unknown
    onDynamic: undefined
  }>
})

it('should allow setting manual errors with standard schema validators on the field level', () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
  })

  const field = new FieldApi({
    form,
    name: 'firstName',
    validators: {
      onChange: z.string(),
    },
  })

  expectTypeOf(field.setErrorMap).parameter(0).toEqualTypeOf<{
    onMount: undefined
    onChange: { message: string }[] | undefined
    onBlur: undefined
    onSubmit: undefined
    onServer: unknown
    onDynamic: undefined
  }>
})


--- packages/form-core/src/FieldGroupApi.ts ---
import { Derived } from '@tanstack/store'
import { concatenatePaths, getBy, makePathArray } from './utils'
import type { Updater } from './utils'
import type {
  FormApi,
  FormAsyncValidateOrFn,
  FormValidateOrFn,
} from './FormApi'
import type { AnyFieldMetaBase, FieldOptions } from './FieldApi'
import type {
  DeepKeys,
  DeepKeysOfType,
  DeepValue,
  FieldsMap,
} from './util-types'
import type {
  FieldManipulator,
  UpdateMetaOptions,
  ValidationCause,
} from './types'

export type AnyFieldGroupApi = FieldGroupApi<
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any
>

export interface FieldGroupState<in out TFieldGroupData> {
  /**
   * The current values of the field group
   */
  values: TFieldGroupData
}

/**
 * An object representing the options for a field group.
 */
export interface FieldGroupOptions<
  in out TFormData,
  in out TFieldGroupData,
  in out TFields extends
    | DeepKeysOfType<TFormData, TFieldGroupData | null | undefined>
    | FieldsMap<TFormData, TFieldGroupData>,
  in out TOnMount extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TSubmitMeta = never,
> {
  form:
    | FormApi<
        TFormData,
        TOnMount,
        TOnChange,
        TOnChangeAsync,
        TOnBlur,
        TOnBlurAsync,
        TOnSubmit,
        TOnSubmitAsync,
        TOnDynamic,
        TOnDynamicAsync,
        TOnServer,
        TSubmitMeta
      >
    | FieldGroupApi<
        any,
        TFormData,
        any,
        any,
        any,
        any,
        any,
        any,
        any,
        any,
        any,
        any,
        any,
        TSubmitMeta
      >
  /**
   * The path to the field group data.
   */
  fields: TFields
  /**
   * The expected subsetValues that the form must provide.
   */
  defaultValues?: TFieldGroupData
  /**
   * onSubmitMeta, the data passed from the handleSubmit handler, to the onSubmit function props
   */
  onSubmitMeta?: TSubmitMeta
}

export class FieldGroupApi<
  in out TFormData,
  in out TFieldGroupData,
  in out TFields extends
    | DeepKeysOfType<TFormData, TFieldGroupData | null | undefined>
    | FieldsMap<TFormData, TFieldGroupData>,
  in out TOnMount extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TSubmitMeta = never,
> implements FieldManipulator<TFieldGroupData, TSubmitMeta> {
  /**
   * The form that called this field group.
   */
  readonly form: FormApi<
    TFormData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TOnServer,
    TSubmitMeta
  >

  readonly fieldsMap: TFields

  /**
   * Get the true name of the field. Not required within `Field` or `AppField`.
   * @private
   */
  getFormFieldName = <TField extends DeepKeys<TFieldGroupData>>(
    subfield: TField,
  ): DeepKeys<TFormData> => {
    if (typeof this.fieldsMap === 'string') {
      return concatenatePaths(this.fieldsMap, subfield)
    }

    const firstAccessor = makePathArray(subfield)[0]
    if (typeof firstAccessor !== 'string') {
      // top-level arrays cannot be mapped
      return ''
    }

    const restOfPath = subfield.slice(firstAccessor.length)
    const formMappedPath =
      // TFields is either a string or this. See guard above.
      (this.fieldsMap as FieldsMap<TFormData, TFieldGroupData>)[
        firstAccessor as keyof TFieldGroupData
      ]

    return concatenatePaths(formMappedPath, restOfPath)
  }

  /**
   * Get the field options with the true form DeepKeys for validators
   * @private
   */
  getFormFieldOptions = <
    TOptions extends FieldOptions<
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any
    >,
  >(
    props: TOptions,
  ): TOptions => {
    const newProps = { ...props }
    const validators = newProps.validators

    newProps.name = this.getFormFieldName(props.name)

    if (
      validators &&
      (validators.onChangeListenTo || validators.onBlurListenTo)
    ) {
      const newValidators = { ...validators }

      const remapListenTo = (listenTo: DeepKeys<any>[] | undefined) => {
        if (!listenTo) return undefined
        return listenTo.map((localFieldName) =>
          this.getFormFieldName(localFieldName),
        )
      }

      newValidators.onChangeListenTo = remapListenTo(
        validators.onChangeListenTo,
      )
      newValidators.onBlurListenTo = remapListenTo(validators.onBlurListenTo)

      newProps.validators = newValidators
    }

    return newProps
  }

  store: Derived<FieldGroupState<TFieldGroupData>>

  get state() {
    return this.store.state
  }

  /**
   * Constructs a new `FieldGroupApi` instance with the given form options.
   */
  constructor(
    opts: FieldGroupOptions<
      TFormData,
      TFieldGroupData,
      TFields,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer,
      TSubmitMeta
    >,
  ) {
    if (opts.form instanceof FieldGroupApi) {
      const group = opts.form
      this.form = group.form as never

      // the DeepKey is already namespaced, so we need to ensure that we reference
      // the form and not the group
      if (typeof opts.fields === 'string') {
        this.fieldsMap = group.getFormFieldName(opts.fields) as TFields
      } else {
        // TypeScript has a tough time with generics being a union for some reason
        const fields = {
          ...(opts.fields as FieldsMap<TFormData, TFieldGroupData>),
        }
        for (const key in fields) {
          fields[key] = group.getFormFieldName(fields[key]) as never
        }
        this.fieldsMap = fields as never
      }
    } else {
      this.form = opts.form
      this.fieldsMap = opts.fields
    }

    this.store = new Derived({
      deps: [this.form.store],
      fn: ({ currDepVals }) => {
        const currFormStore = currDepVals[0]
        let values: TFieldGroupData
        if (typeof this.fieldsMap === 'string') {
          // all values live at that name, so we can directly fetch it
          values = getBy(currFormStore.values, this.fieldsMap)
        } else {
          // we need to fetch the values from all places where they were mapped from
          values = {} as never
          const fields: Record<keyof TFieldGroupData, string> = this
            .fieldsMap as never
          for (const key in fields) {
            values[key] = getBy(currFormStore.values, fields[key])
          }
        }

        return {
          values,
        }
      },
    })
  }

  /**
   * Mounts the field group instance to listen to value changes.
   */
  mount = () => {
    const cleanup = this.store.mount()

    return cleanup
  }

  /**
   * 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<TFieldGroupData, any[]>,
  >(
    field: TField,
    index: number,
    cause: ValidationCause,
  ) => {
    return this.form.validateArrayFieldsStartingFrom(
      this.getFormFieldName(field),
      index,
      cause,
    )
  }

  /**
   * Validates a specified field in the form using the correct handlers for a given validation type.
   */
  validateField = <TField extends DeepKeys<TFieldGroupData>>(
    field: TField,
    cause: ValidationCause,
  ) => {
    return this.form.validateField(this.getFormFieldName(field), cause)
  }

  /**
   * Handles the form submission, performs validation, and calls the appropriate onSubmit or onSubmitInvalid callbacks.
   */
  handleSubmit(): Promise<void>
  handleSubmit(submitMeta: TSubmitMeta): Promise<void>
  async handleSubmit(submitMeta?: TSubmitMeta): Promise<void> {
    // cast is required since the implementation isn't one of the two overloads
    return this.form.handleSubmit(submitMeta as any)
  }

  /**
   * Gets the value of the specified field.
   */
  getFieldValue = <TField extends DeepKeys<TFieldGroupData>>(
    field: TField,
  ): DeepValue<TFieldGroupData, TField> => {
    return this.form.getFieldValue(this.getFormFieldName(field)) as DeepValue<
      TFieldGroupData,
      TField
    >
  }

  /**
   * Gets the metadata of the specified field.
   */
  getFieldMeta = <TField extends DeepKeys<TFieldGroupData>>(field: TField) => {
    return this.form.getFieldMeta(this.getFormFieldName(field))
  }

  /**
   * Updates the metadata of the specified field.
   */
  setFieldMeta = <TField extends DeepKeys<TFieldGroupData>>(
    field: TField,
    updater: Updater<AnyFieldMetaBase>,
  ) => {
    return this.form.setFieldMeta(this.getFormFieldName(field), updater)
  }

  /**
   * Sets the value of the specified field and optionally updates the touched state.
   */
  setFieldValue = <TField extends DeepKeys<TFieldGroupData>>(
    field: TField,
    updater: Updater<DeepValue<TFieldGroupData, TField>>,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.setFieldValue(
      this.getFormFieldName(field) as never,
      updater as never,
      opts,
    )
  }

  /**
   * Delete a field and its subfields.
   */
  deleteField = <TField extends DeepKeys<TFieldGroupData>>(field: TField) => {
    return this.form.deleteField(this.getFormFieldName(field))
  }

  /**
   * Pushes a value into an array field.
   */
  pushFieldValue = <TField extends DeepKeysOfType<TFieldGroupData, any[]>>(
    field: TField,
    value: DeepValue<TFieldGroupData, TField> extends any[]
      ? DeepValue<TFieldGroupData, TField>[number]
      : never,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.pushFieldValue(
      this.getFormFieldName(field),
      // since unknown doesn't extend an array, it types `value` as never.
      value as never,
      opts,
    )
  }

  /**
   * Insert a value into an array field at the specified index.
   */
  insertFieldValue = async <
    TField extends DeepKeysOfType<TFieldGroupData, any[]>,
  >(
    field: TField,
    index: number,
    value: DeepValue<TFieldGroupData, TField> extends any[]
      ? DeepValue<TFieldGroupData, TField>[number]
      : never,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.insertFieldValue(
      this.getFormFieldName(field),
      index,
      // since unknown doesn't extend an array, it types `value` as never.
      value as never,
      opts,
    )
  }

  /**
   * Replaces a value into an array field at the specified index.
   */
  replaceFieldValue = async <
    TField extends DeepKeysOfType<TFieldGroupData, any[]>,
  >(
    field: TField,
    index: number,
    value: DeepValue<TFieldGroupData, TField> extends any[]
      ? DeepValue<TFieldGroupData, TField>[number]
      : never,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.replaceFieldValue(
      this.getFormFieldName(field),
      index,
      // since unknown doesn't extend an array, it types `value` as never.
      value as never,
      opts,
    )
  }

  /**
   * Removes a value from an array field at the specified index.
   */
  removeFieldValue = async <
    TField extends DeepKeysOfType<TFieldGroupData, any[]>,
  >(
    field: TField,
    index: number,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.removeFieldValue(this.getFormFieldName(field), index, opts)
  }

  /**
   * Swaps the values at the specified indices within an array field.
   */
  swapFieldValues = <TField extends DeepKeysOfType<TFieldGroupData, any[]>>(
    field: TField,
    index1: number,
    index2: number,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.swapFieldValues(
      this.getFormFieldName(field),
      index1,
      index2,
      opts,
    )
  }

  /**
   * Moves the value at the first specified index to the second specified index within an array field.
   */
  moveFieldValues = <TField extends DeepKeysOfType<TFieldGroupData, any[]>>(
    field: TField,
    index1: number,
    index2: number,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.moveFieldValues(
      this.getFormFieldName(field),
      index1,
      index2,
      opts,
    )
  }

  clearFieldValues = <TField extends DeepKeysOfType<TFieldGroupData, any[]>>(
    field: TField,
    opts?: UpdateMetaOptions,
  ) => {
    return this.form.clearFieldValues(this.getFormFieldName(field), opts)
  }

  /**
   * Resets the field value and meta to default state
   */
  resetField = <TField extends DeepKeys<TFieldGroupData>>(field: TField) => {
    return this.form.resetField(this.getFormFieldName(field))
  }

  validateAllFields = (cause: ValidationCause) =>
    this.form.validateAllFields(cause)
}


--- packages/form-core/tests/FieldGroupApi.spec.ts ---
import { describe, expect, it, vi } from 'vitest'
import { FieldApi, FieldGroupApi, FormApi } from '../src/index'

describe('field group api', () => {
  type Person = {
    name: string
    age: number
  }
  type FormValues = {
    people: Person[]
    name: string
    age: number
    relatives: {
      father: Person
    }
  }

  it('should inherit defaultValues from the form', () => {
    const defaultValues: FormValues = {
      name: 'Do not access',
      age: -1,
      people: [
        {
          name: 'fieldGroup one',
          age: 1,
        },
        {
          name: 'fieldGroup two',
          age: 2,
        },
      ],
      relatives: {
        father: {
          name: 'fieldGroup three',
          age: 3,
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup1 = new FieldGroupApi({
      form,
      fields: 'people[0]',
      defaultValues: {} as Person,
    })
    const fieldGroup2 = new FieldGroupApi({
      form,
      fields: 'people[1]',
      defaultValues: {} as Person,
    })
    const fieldGroup3 = new FieldGroupApi({
      form,
      fields: 'relatives.father',
      defaultValues: {} as Person,
    })
    fieldGroup1.mount()
    fieldGroup2.mount()
    fieldGroup3.mount()

    expect(fieldGroup1.state).toMatchObject({
      values: {
        name: 'fieldGroup one',
        age: 1,
      },
    })
    expect(fieldGroup2.state).toMatchObject({
      values: {
        name: 'fieldGroup two',
        age: 2,
      },
    })
    expect(fieldGroup3.state).toMatchObject({
      values: {
        name: 'fieldGroup three',
        age: 3,
      },
    })
  })

  it('should have the state synced with the form', () => {
    const defaultValues: FormValues = {
      name: 'Do not access',
      age: -1,
      people: [],
      relatives: {
        father: {
          name: 'father',
          age: 10,
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      form,
      defaultValues: {} as Person,
      fields: 'relatives.father',
    })
    fieldGroup.mount()

    expect(fieldGroup.state.values).toEqual(form.state.values.relatives.father)

    form.setFieldValue('relatives.father.name', 'New name')
    form.setFieldValue('relatives.father.age', 50)

    expect(fieldGroup.state.values).toEqual(form.state.values.relatives.father)

    fieldGroup.setFieldValue('name', 'Second new name')
    fieldGroup.setFieldValue('age', 100)

    expect(fieldGroup.state.values).toEqual(form.state.values.relatives.father)
    fieldGroup.form.reset()

    expect(fieldGroup.state.values).toEqual(form.state.values.relatives.father)
  })

  it('should validate the right field from the form', () => {
    const defaultValues: FormValues = {
      name: '',
      age: 0,
      people: [
        {
          name: 'fieldGroup one',
          age: 1,
        },
        {
          name: 'fieldGroup two',
          age: 2,
        },
      ],
      relatives: {
        father: {
          name: 'fieldGroup three',
          age: 3,
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const field1 = new FieldApi({
      form,
      name: 'people[0].age',
      validators: {
        onChange: () => 'Field 1',
      },
    })
    const field2 = new FieldApi({
      form,
      name: 'people[1].age',
      validators: {
        onChange: () => 'Field 2',
      },
    })
    const field3 = new FieldApi({
      form,
      name: 'relatives.father.age',
      validators: {
        onChange: () => 'Field 3',
      },
    })

    field1.mount()
    field2.mount()
    field3.mount()

    const fieldGroup1 = new FieldGroupApi({
      form,
      fields: 'people[0]',
      defaultValues: {} as Person,
    })
    const fieldGroup2 = new FieldGroupApi({
      form,
      fields: 'people[1]',
      defaultValues: {} as Person,
    })
    const fieldGroup3 = new FieldGroupApi({
      form,
      fields: 'relatives.father',
      defaultValues: {} as Person,
    })
    fieldGroup1.mount()
    fieldGroup2.mount()
    fieldGroup3.mount()

    fieldGroup1.validateField('age', 'change')

    expect(field1.state.meta.errors).toEqual(['Field 1'])
    expect(field2.state.meta.errors).toEqual([])
    expect(field3.state.meta.errors).toEqual([])

    fieldGroup2.validateField('age', 'change')

    expect(field1.state.meta.errors).toEqual(['Field 1'])
    expect(field2.state.meta.errors).toEqual(['Field 2'])
    expect(field3.state.meta.errors).toEqual([])

    fieldGroup3.validateField('age', 'change')

    expect(field1.state.meta.errors).toEqual(['Field 1'])
    expect(field2.state.meta.errors).toEqual(['Field 2'])
    expect(field3.state.meta.errors).toEqual(['Field 3'])
  })

  it('should get the right field value from the nested field', () => {
    const defaultValues: FormValues = {
      name: '',
      age: 0,
      people: [
        {
          name: 'fieldGroup one',
          age: 1,
        },
        {
          name: 'fieldGroup two',
          age: 2,
        },
      ],
      relatives: {
        father: {
          name: 'fieldGroup three',
          age: 3,
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup1 = new FieldGroupApi({
      form,
      fields: 'people[0]',
      defaultValues: {} as Person,
    })
    const fieldGroup2 = new FieldGroupApi({
      form,
      fields: 'people[1]',
      defaultValues: {} as Person,
    })
    const fieldGroup3 = new FieldGroupApi({
      form,
      fields: 'relatives.father',
      defaultValues: {} as Person,
    })
    fieldGroup1.mount()
    fieldGroup2.mount()
    fieldGroup3.mount()

    expect(fieldGroup1.getFieldValue('age')).toBe(1)
    expect(fieldGroup1.getFieldValue('name')).toBe('fieldGroup one')

    expect(fieldGroup2.getFieldValue('age')).toBe(2)
    expect(fieldGroup2.getFieldValue('name')).toBe('fieldGroup two')

    expect(fieldGroup3.getFieldValue('age')).toBe(3)
    expect(fieldGroup3.getFieldValue('name')).toBe('fieldGroup three')
  })

  it('should get the correct field Meta from the nested field', () => {
    const defaultValues: FormValues = {
      name: '',
      age: 0,
      people: [
        {
          name: 'fieldGroup one',
          age: 1,
        },
        {
          name: 'fieldGroup two',
          age: 2,
        },
      ],
      relatives: {
        father: {
          name: 'fieldGroup three',
          age: 3,
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const field1 = new FieldApi({
      form,
      name: 'people[0].age',
    })
    const field2 = new FieldApi({
      form,
      name: 'people[1].age',
    })
    const field3 = new FieldApi({
      form,
      name: 'relatives.father.age',
      validators: {
        onMount: () => 'Error',
      },
    })

    field1.mount()
    field2.mount()
    field3.mount()

    field1.handleChange(0)
    field2.handleBlur()

    const fieldGroup1 = new FieldGroupApi({
      form,
      fields: 'people[0]',
      defaultValues: {} as Person,
    })
    const fieldGroup2 = new FieldGroupApi({
      form,
      fields: 'people[1]',
      defaultValues: {} as Person,
    })
    const fieldGroup3 = new FieldGroupApi({
      form,
      fields: 'relatives.father',
      defaultValues: {} as Person,
    })
    fieldGroup1.mount()
    fieldGroup2.mount()
    fieldGroup3.mount()

    expect(fieldGroup1.getFieldMeta('age')?.isValid).toBe(true)
    expect(fieldGroup2.getFieldMeta('age')?.isValid).toBe(true)
    expect(fieldGroup3.getFieldMeta('age')?.isValid).toBe(false)

    expect(fieldGroup1.getFieldMeta('age')?.isDirty).toBe(true)
    expect(fieldGroup2.getFieldMeta('age')?.isDirty).toBe(false)
    expect(fieldGroup3.getFieldMeta('age')?.isDirty).toBe(false)

    expect(fieldGroup1.getFieldMeta('age')?.isBlurred).toBe(false)
    expect(fieldGroup2.getFieldMeta('age')?.isBlurred).toBe(true)
    expect(fieldGroup3.getFieldMeta('age')?.isBlurred).toBe(false)
  })

  it('should be compliant with top level array defaultValues', () => {
    const form = new FormApi({
      defaultValues: { people: [{ name: 'Default' }, { name: 'Default' }] },
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      form,
      defaultValues: [{ name: '' }],
      fields: 'people',
    })
    fieldGroup.mount()

    fieldGroup.setFieldValue('[0]', { name: 'Override One' })
    fieldGroup.setFieldValue('[1].name', 'Override Two')

    expect(form.getFieldValue('people[0].name')).toBe('Override One')
    expect(form.getFieldValue('people[1].name')).toBe('Override Two')
  })

  it('should forward validateArrayFieldsStartingFrom to form', async () => {
    vi.useFakeTimers()
    const defaultValues = {
      people: {
        names: [
          {
            name: '',
          },
          {
            name: '',
          },
          {
            name: '',
          },
        ],
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const field0 = new FieldApi({
      form,
      name: 'people.names[0].name',
    })
    const field1 = new FieldApi({
      form,
      name: 'people.names[1].name',
    })
    const field2 = new FieldApi({
      form,
      name: 'people.names[2].name',
    })
    field0.mount()
    field1.mount()
    field2.mount()

    const fieldGroup = new FieldGroupApi({
      form,
      defaultValues: {
        names: [{ name: '' }],
      },
      fields: 'people',
    })
    fieldGroup.mount()

    fieldGroup.validateArrayFieldsStartingFrom('names', 1, 'change')

    await vi.runAllTimersAsync()

    expect(field0.getMeta().isTouched).toBe(false)
    expect(field1.getMeta().isTouched).toBe(true)
    expect(field2.getMeta().isTouched).toBe(true)
  })

  it('should forward handleSubmit to the form', async () => {
    vi.useFakeTimers()

    const defaultValues = {
      person: {
        name: '',
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      defaultValues: { name: '' },
      form,
      fields: 'person',
    })
    fieldGroup.mount()

    fieldGroup.handleSubmit()

    await vi.runAllTimersAsync()

    expect(form.state.isSubmitted).toBe(true)
    expect(form.state.isSubmitSuccessful).toBe(true)
  })

  it('should forward resetField to the form', () => {
    const defaultValues = {
      nested: {
        field: {
          name: '',
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      defaultValues: { name: '' },
      form,
      fields: 'nested.field',
    })
    fieldGroup.mount()

    fieldGroup.setFieldValue('name', 'Nested')

    expect(form.state.values.nested.field.name).toEqual('Nested')

    fieldGroup.resetField('name')
    expect(form.state.values.nested.field.name).toEqual('')
  })

  it('should forward deleteField to the form', () => {
    const defaultValues = {
      nested: {
        field: {
          name: '',
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      defaultValues: { name: '' },
      form,
      fields: 'nested.field',
    })
    fieldGroup.mount()

    fieldGroup.deleteField('name')

    expect(form.state.values.nested.field.name).toBeUndefined()
  })

  it('should forward array methods to the form', async () => {
    vi.useFakeTimers()
    const defaultValues = {
      people: {
        names: [
          {
            name: '',
          },
          {
            name: '',
          },
          {
            name: '',
          },
        ],
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const field0 = new FieldApi({
      form,
      name: 'people.names[0].name',
    })
    const field1 = new FieldApi({
      form,
      name: 'people.names[1].name',
    })
    const field2 = new FieldApi({
      form,
      name: 'people.names[2].name',
    })
    field0.mount()
    field1.mount()
    field2.mount()

    const fieldGroup = new FieldGroupApi({
      defaultValues: { names: [{ name: '' }] },
      form,
      fields: 'people',
    })
    fieldGroup.mount()

    fieldGroup.validateArrayFieldsStartingFrom('names', 1, 'change')

    await vi.runAllTimersAsync()

    expect(field0.getMeta().isTouched).toBe(false)
    expect(field1.getMeta().isTouched).toBe(true)
    expect(field2.getMeta().isTouched).toBe(true)

    fieldGroup.pushFieldValue('names', { name: 'Push' })

    expect(form.getFieldValue('people.names')).toEqual([
      {
        name: '',
      },
      {
        name: '',
      },
      {
        name: '',
      },
      {
        name: 'Push',
      },
    ])

    fieldGroup.insertFieldValue('names', 1, { name: 'Insert' })

    expect(form.getFieldValue('people.names')).toEqual([
      {
        name: '',
      },
      {
        name: 'Insert',
      },
      {
        name: '',
      },
      {
        name: '',
      },
      {
        name: 'Push',
      },
    ])

    fieldGroup.replaceFieldValue('names', 2, { name: 'Replace' })

    expect(form.getFieldValue('people.names')).toEqual([
      {
        name: '',
      },
      {
        name: 'Insert',
      },
      {
        name: 'Replace',
      },
      {
        name: '',
      },
      {
        name: 'Push',
      },
    ])

    fieldGroup.removeFieldValue('names', 3)

    expect(form.getFieldValue('people.names')).toEqual([
      {
        name: '',
      },
      {
        name: 'Insert',
      },
      {
        name: 'Replace',
      },
      {
        name: 'Push',
      },
    ])

    fieldGroup.swapFieldValues('names', 2, 3)

    expect(form.getFieldValue('people.names')).toEqual([
      {
        name: '',
      },
      {
        name: 'Insert',
      },
      {
        name: 'Push',
      },
      {
        name: 'Replace',
      },
    ])

    fieldGroup.moveFieldValues('names', 0, 2)

    expect(form.getFieldValue('people.names')).toEqual([
      {
        name: 'Insert',
      },
      {
        name: 'Push',
      },
      {
        name: '',
      },
      {
        name: 'Replace',
      },
    ])

    fieldGroup.clearFieldValues('names')

    expect(form.getFieldValue('people.names')).toEqual([])
  })

  it('should allow nesting form fieldGroupes within each other', () => {
    type Nested = {
      firstName: string
    }
    type Wrapper = {
      field: Nested
    }
    type FormVals = {
      form: Wrapper
      unrelated: { something: { lastName: string } }
    }

    const defaultValues: FormVals = {
      form: {
        field: {
          firstName: 'Test',
        },
      },
      unrelated: {
        something: {
          lastName: '',
        },
      },
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroupWrap = new FieldGroupApi({
      defaultValues: defaultValues.form,
      form,
      fields: 'form',
    })
    fieldGroupWrap.mount()

    const fieldGroupNested = new FieldGroupApi({
      defaultValues: defaultValues.form.field,
      form: fieldGroupWrap,
      fields: 'field',
    })
    fieldGroupNested.mount()

    expect(fieldGroupNested.form).toEqual(fieldGroupWrap.form)
    expect(fieldGroupNested.state.values).toEqual(
      fieldGroupWrap.state.values.field,
    )
    expect(fieldGroupNested.state.values).toEqual(form.state.values.form.field)
  })

  it('should allow remapping values for fieldGroups', () => {
    type FormVals = {
      a: string
      b: string
    }

    const defaultValues: FormVals = {
      a: 'A',
      b: 'B',
    }
    const group = {
      firstName: '',
      lastName: '',
    }

    const form = new FormApi({
      defaultValues,
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      form,
      fields: {
        firstName: 'a',
        lastName: 'b',
      },
      defaultValues: group,
    })
    fieldGroup.mount()

    expect(fieldGroup.state.values.firstName).toBe('A')
    expect(fieldGroup.state.values.lastName).toBe('B')
    expect(fieldGroup.getFormFieldName('firstName')).toBe('a')
    expect(fieldGroup.getFormFieldName('lastName')).toBe('b')
  })

  it('should not crash on top-level array defaultValues', () => {
    const defaultValues = {
      firstName: '',
      lastName: '',
    }

    const form = new FormApi({
      defaultValues: { a: '', b: '' },
    })
    form.mount()

    const fieldGroup = new FieldGroupApi({
      defaultValues: [defaultValues],
      form,
      // @ts-expect-error Typing saves us here, but this edge case needs to be guarded either way
      fields: {},
    })
    fieldGroup.mount()

    expect(() =>
      fieldGroup.getFormFieldName('[0].firstName'),
    ).not.toThrowError()
    expect(fieldGroup.getFormFieldName('[0].firstName')).toBeDefined()
  })

  it('should allow remapping with nested field groups', () => {
    const formValues = {
      a: 'A',
      b: 'B',
    }

    const form = new FormApi({
      defaultValues: formValues,
    })
    form.mount()

    const groupWrapper = new FieldGroupApi({
      form,
      defaultValues: { foo: '', bar: '' },
      fields: {
        bar: 'b',
        foo: 'a',
      },
    })
    groupWrapper.mount()

    const groupNested = new FieldGroupApi({
      form: groupWrapper,
      defaultValues: { shouldBeA: '', shouldBeB: '' },
      fields: {
        shouldBeA: 'foo',
        shouldBeB: 'bar',
      },
    })
    groupNested.mount()

    expect(groupNested.state.values.shouldBeA).toBe('A')
    expect(groupNested.state.values.shouldBeB).toBe('B')
    expect(groupNested.getFormFieldName('shouldBeA')).toBe('a')
    expect(groupNested.getFormFieldName('shouldBeB')).toBe('b')
  })

  it('should allow setting and resetting field meta in field groups', () => {
    const form = new FormApi({
      defaultValues: {
        person: {
          firstName: '',
        },
      },
    })
    form.mount()

    const group = new FieldGroupApi({
      defaultValues: { firstName: '' },
      form,
      fields: 'person',
    })
    group.mount()

    group.setFieldMeta('firstName', (p) => ({ ...p, isTouched: true }))

    expect(form.getFieldMeta('person.firstName')?.isTouched).toBe(true)
  })

  it('should forward validateAllFields to the form', async () => {
    vi.useFakeTimers()
    const form = new FormApi({
      defaultValues: {
        person: {
          firstName: '',
        },
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'person.firstName',
      validators: {
        onChange: () => 'Error',
      },
    })
    field.mount()

    const group = new FieldGroupApi({
      defaultValues: { firstName: '' },
      form,
      fields: 'person',
    })
    group.mount()

    group.validateAllFields('change')

    await vi.runAllTimersAsync()

    expect(form.state.isValid).toBe(false)
    expect(field.state.meta.isValid).toBe(false)
    expect(form.getAllErrors().fields['person.firstName'].errors).toEqual([
      'Error',
    ])
  })

  it('should generate form field names properly with nested objects', () => {
    // https://github.com/TanStack/form/issues/1645
    const form = new FormApi({
      defaultValues: {
        complexValue: {
          prop1: 0,
          prop2: 0,
        },
      },
    })
    form.mount()

    const group = new FieldGroupApi({
      defaultValues: {
        complexValue: {
          prop1: 0,
          prop2: 0,
        },
      },
      form,
      fields: {
        complexValue: 'complexValue',
      },
    })
    group.mount()

    expect(group.getFormFieldName('complexValue.prop1')).toBe(
      'complexValue.prop1',
    )
  })

  it('should remap the name of field options correctly', () => {
    const form = new FormApi({
      defaultValues: {
        user: {
          profile: {
            personal: {
              firstName: '',
              lastName: '',
              email: '',
            },
            preferences: {
              theme: 'light',
              notifications: true,
            },
          },
          settings: {
            privacy: {
              shareData: false,
              allowMarketing: true,
            },
          },
        },
        alternateProfile: {
          firstName: '',
          lastName: '',
          email: '',
        },
      },
    })
    form.mount()

    const fieldGroupString = new FieldGroupApi({
      form,
      fields: 'user.profile.personal',
      defaultValues: { firstName: '' },
    })
    fieldGroupString.mount()

    const props1 = {
      name: 'firstName',
    }
    const remappedProps1 = fieldGroupString.getFormFieldOptions(props1)
    expect(remappedProps1.name).toBe('user.profile.personal.firstName')

    const fieldGroupObject = new FieldGroupApi({
      form,
      fields: {
        firstName: 'alternateProfile.firstName',
        lastName: 'alternateProfile.lastName',
        email: 'alternateProfile.email',
      },
      defaultValues: { firstName: '' },
    })
    fieldGroupObject.mount()

    const props2 = {
      name: 'firstName',
    }
    const remappedProps2 = fieldGroupObject.getFormFieldOptions(props2)
    expect(remappedProps2.name).toBe('alternateProfile.firstName')
  })

  it('should remap listener paths with its remapFieldProps method', () => {
    const form = new FormApi({
      defaultValues: {
        user: {
          profile: {
            personal: {
              firstName: '',
              lastName: '',
              email: '',
            },
            preferences: {
              theme: 'light',
              notifications: true,
            },
          },
          settings: {
            privacy: {
              shareData: false,
              allowMarketing: true,
            },
          },
        },
        alternateProfile: {
          firstName: '',
          lastName: '',
          email: '',
        },
      },
    })
    form.mount()

    const fieldGroupString = new FieldGroupApi({
      form,
      fields: 'user.profile.personal',
      defaultValues: { firstName: '', lastName: '', email: '' },
    })
    fieldGroupString.mount()

    const props1 = {
      name: 'email',
      validators: {
        onChangeListenTo: ['firstName'],
        onBlurListenTo: ['lastName'],
      },
    }
    const remappedProps1 = fieldGroupString.getFormFieldOptions(props1)
    expect(remappedProps1.validators.onChangeListenTo).toEqual([
      'user.profile.personal.firstName',
    ])
    expect(remappedProps1.validators.onBlurListenTo).toEqual([
      'user.profile.personal.lastName',
    ])

    const fieldGroupObject = new FieldGroupApi({
      form,
      fields: {
        firstName: 'alternateProfile.firstName',
        lastName: 'alternateProfile.lastName',
        email: 'alternateProfile.email',
      },
      defaultValues: { firstName: '', lastName: '', email: '' },
    })
    fieldGroupObject.mount()

    const props2 = {
      name: 'email',
      validators: {
        onChangeListenTo: ['firstName', 'lastName'],
      },
    }
    const remappedProps2 = fieldGroupObject.getFormFieldOptions(props2)
    expect(remappedProps2.validators.onChangeListenTo).toEqual([
      'alternateProfile.firstName',
      'alternateProfile.lastName',
    ])
  })
})


--- packages/form-core/tests/FieldGroupApi.test-d.ts ---
import { describe, expectTypeOf, it } from 'vitest'
import { FieldGroupApi, FormApi } from '../src'

describe('fieldGroupApi', () => {
  it('should have the correct properties based on defaultValues', () => {
    const form = new FormApi({
      defaultValues: {
        a: '',
        b: '',
      },
    })

    const group = new FieldGroupApi({
      form,
      defaultValues: { foo: '', bar: '' },
      fields: {
        foo: 'a',
        bar: 'b',
      },
    })

    expectTypeOf(group.state.values).toEqualTypeOf<{
      foo: string
      bar: string
    }>()
    expectTypeOf(group.getFieldValue)
      .parameter(0)
      .toEqualTypeOf<'foo' | 'bar'>()
  })

  it('should have strict typing for meta if specified', () => {
    const defaultValues = {
      a: '',
      b: '',
    }
    const groupValues = {
      foo: '',
    }
    const fields = {
      foo: 'a',
    } as const

    const correctMeta = {
      action: '',
    }
    const wrongMeta = {
      action: 0,
    }

    const formNoMeta = new FormApi({
      defaultValues,
    })

    const formWithMeta = new FormApi({
      defaultValues,
      onSubmitMeta: correctMeta,
    })

    const formWithWrongMeta = new FormApi({
      defaultValues,
      onSubmitMeta: wrongMeta,
    })

    // When no meta is specified, any meta should do
    const correctGroup1 = new FieldGroupApi({
      form: formNoMeta,
      defaultValues: groupValues,
      fields,
    })
    const correctGroup2 = new FieldGroupApi({
      form: formWithMeta,
      defaultValues: groupValues,
      fields,
    })
    const correctGroup3 = new FieldGroupApi({
      form: formWithWrongMeta,
      defaultValues: groupValues,
      fields,
    })

    const wrongGroup1 = new FieldGroupApi({
      // @ts-expect-error
      form: formNoMeta,
      defaultValues: groupValues,
      fields,
      onSubmitMeta: correctMeta,
    })
    const correctGroup4 = new FieldGroupApi({
      form: formWithMeta,
      defaultValues: groupValues,
      fields,
      onSubmitMeta: correctMeta,
    })
    const wrongGroup2 = new FieldGroupApi({
      form: formWithWrongMeta,
      defaultValues: groupValues,
      fields,
      // @ts-expect-error
      onSubmitMeta: correctMeta,
    })
  })

  it('should allow wrapping groups in other groups', () => {
    const defaultValues = {
      a: '',
      b: '',
    }

    const groupWrapperValues = {
      foo: '',
    }

    const groupNestedValues = {
      bar: '',
    }

    const form = new FormApi({
      defaultValues,
    })

    const fieldGroupWrapper = new FieldGroupApi({
      defaultValues: groupWrapperValues,
      form,
      fields: {
        foo: 'a',
      },
    })

    const fieldGroupNested = new FieldGroupApi({
      defaultValues: groupNestedValues,
      form: fieldGroupWrapper,
      fields: {
        bar: 'foo',
      },
    })
  })

  it('should allow mapping fields to field groups', () => {
    const defaultValues = {
      a: '',
      b: '',
      c: 0,
      d: { e: '', f: 0 },
    }

    const form = new FormApi({
      defaultValues,
    })

    const group = new FieldGroupApi({
      form,
      defaultValues: { canBeA: '', orB: '', notC: '', butE: '', notF: '' },
      fields: {
        canBeA: 'a',
        orB: 'b',
        // @ts-expect-error
        notC: 'c',
        butE: 'd.e',
        // @ts-expect-error
        notF: 'f',
      },
    })

    const prefixGroup = new FieldGroupApi({
      form,
      defaultValues: { e: '', f: 0 },
      fields: 'd',
    })
  })

  it('should allow null and undefined for fields when string', () => {
    type FormValues = {
      foo:
        | {
            bar: string
          }
        | null
        | undefined
    }

    const defaultValues: FormValues = {
      foo: { bar: '' },
    }

    const form = new FormApi({
      defaultValues,
    })

    const group = new FieldGroupApi({
      form,
      defaultValues: { bar: '' },
      fields: 'foo',
    })

    const wrongGroup = new FieldGroupApi({
      form,
      defaultValues: { bar: '' },
      fields: {
        // @ts-expect-error
        bar: 'foo.bar',
      },
    })
  })
})


--- packages/form-core/src/FormApi.ts ---
import { Derived, Store, batch } from '@tanstack/store'
import {
  deleteBy,
  determineFormLevelErrorSourceAndValue,
  evaluate,
  functionalUpdate,
  getAsyncValidatorArray,
  getBy,
  getSyncValidatorArray,
  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'

// types
import type { ValidationLogicFn } from './ValidationLogic'
import type {
  StandardSchemaV1,
  StandardSchemaV1Issue,
  TStandardSchemaValidatorValue,
} from './standardSchemaValidator'
import type {
  AnyFieldApi,
  AnyFieldMeta,
  AnyFieldMetaBase,
  FieldApi,
} from './FieldApi'
import type {
  ExtractGlobalFormError,
  FieldManipulator,
  FormValidationError,
  FormValidationErrorMap,
  ListenerCause,
  UpdateMetaOptions,
  ValidationCause,
  ValidationError,
  ValidationErrorMap,
  ValidationErrorMapKeys,
} from './types'
import type { DeepKeys, DeepKeysOfType, DeepValue } from './util-types'
import type { Updater } from './utils'

/**
 * @private
 */
// TODO: Add the `Unwrap` type to the errors
type FormErrorMapFromValidator<
  TFormData,
  TOnMount extends undefined | FormValidateOrFn<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
> = Partial<
  Record<
    DeepKeys<TFormData>,
    ValidationErrorMap<
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync
    >
  >
>

export type FormValidateFn<TFormData> = (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<TFormData> =
  | FormValidateFn<TFormData>
  | StandardSchemaV1<TFormData, unknown>

export type UnwrapFormValidateOrFn<
  TValidateOrFn extends undefined | FormValidateOrFn<any>,
> = [TValidateOrFn] extends [FormValidateFn<any>]
  ? ExtractGlobalFormError<ReturnType<TValidateOrFn>>
  : [TValidateOrFn] extends [StandardSchemaV1<any, any>]
    ? Record<string, StandardSchemaV1Issue[]>
    : undefined

/**
 * @private
 */
export type FormValidateAsyncFn<TFormData> = (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<unknown>

export type FormValidator<TFormData, TType, TFn = unknown> = {
  validate(options: { value: TType }, fn: TFn): ValidationError
  validateAsync(
    options: { value: TType },
    fn: TFn,
  ): Promise<FormValidationError<TFormData>>
}

type ValidationPromiseResult<TFormData> =
  | {
      fieldErrors: Partial<Record<DeepKeys<TFormData>, ValidationError>>
      errorMapKey: ValidationErrorMapKeys
    }
  | undefined

/**
 * @private
 */
export type FormAsyncValidateOrFn<TFormData> =
  | FormValidateAsyncFn<TFormData>
  | StandardSchemaV1<TFormData, unknown>

export type UnwrapFormAsyncValidateOrFn<
  TValidateOrFn extends undefined | FormAsyncValidateOrFn<any>,
> = [TValidateOrFn] extends [FormValidateAsyncFn<any>]
  ? ExtractGlobalFormError<Awaited<ReturnType<TValidateOrFn>>>
  : [TValidateOrFn] extends [StandardSchemaV1<any, any>]
    ? Record<string, StandardSchemaV1Issue[]>
    : undefined

export interface FormValidators<
  TFormData,
  TOnMount extends undefined | FormValidateOrFn<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
> {
  /**
   * Optional function that fires as soon as the component mounts.
   */
  onMount?: TOnMount
  /**
   * Optional function that checks the validity of your data whenever a value changes
   */
  onChange?: TOnChange
  /**
   * 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?: TOnBlur
  /**
   * Optional onBlur asynchronous validation method for when a field loses focus returns a ` FormValidationError` or a promise of `Promise<FormValidationError>`
   */
  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?: TOnSubmit
  onSubmitAsync?: TOnSubmitAsync
  onDynamic?: TOnDynamic
  onDynamicAsync?: TOnDynamicAsync
  onDynamicAsyncDebounceMs?: number
}

/**
 * @private
 */
export interface FormTransform<
  TFormData,
  TOnMount extends undefined | FormValidateOrFn<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  TSubmitMeta = never,
> {
  fn: (
    formBase: FormApi<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer,
      TSubmitMeta
    >,
  ) => FormApi<
    TFormData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TOnServer,
    TSubmitMeta
  >
  deps: unknown[]
}

export interface FormListeners<
  TFormData,
  TOnMount extends undefined | FormValidateOrFn<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  TSubmitMeta = never,
> {
  onChange?: (props: {
    formApi: FormApi<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer,
      TSubmitMeta
    >
    fieldApi: AnyFieldApi
  }) => void
  onChangeDebounceMs?: 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
}

/**
 * An object representing the base properties of a form, unrelated to any validators
 */
export interface BaseFormOptions<in out TFormData, in out TSubmitMeta = never> {
  /**
   * 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<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TSubmitMeta = never,
> extends BaseFormOptions<TFormData, TSubmitMeta> {
  /**
   * 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<any>`
   */
  onSubmit?: (props: {
    value: TFormData
    formApi: FormApi<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer,
      TSubmitMeta
    >
    meta: TSubmitMeta
  }) => any | Promise<any>
  /**
   * 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
  transform?: FormTransform<
    NoInfer<TFormData>,
    NoInfer<TOnMount>,
    NoInfer<TOnChange>,
    NoInfer<TOnChangeAsync>,
    NoInfer<TOnBlur>,
    NoInfer<TOnBlurAsync>,
    NoInfer<TOnSubmit>,
    NoInfer<TOnSubmitAsync>,
    NoInfer<TOnDynamic>,
    NoInfer<TOnDynamicAsync>,
    NoInfer<TOnServer>,
    NoInfer<TSubmitMeta>
  >
}

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 field information for a specific field within the form.
 */
export type FieldInfo<TFormData> = {
  /**
   * An instance of the FieldAPI.
   */
  instance: FieldApi<
    TFormData,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any,
    any
  > | null
  /**
   * A record of field validation internal handling.
   */
  validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
}

/**
 * An object representing the current state of the form.
 */
export type BaseFormState<
  in out TFormData,
  in out TOnMount extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> = {
  /**
   * The current values of the form fields.
   */
  values: TFormData
  /**
   * The error map for the form itself.
   */
  errorMap: ValidationErrorMap<
    UnwrapFormValidateOrFn<TOnMount>,
    UnwrapFormValidateOrFn<TOnChange>,
    UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
    UnwrapFormValidateOrFn<TOnBlur>,
    UnwrapFormAsyncValidateOrFn<TOnBlurAsync>,
    UnwrapFormValidateOrFn<TOnSubmit>,
    UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>,
    UnwrapFormValidateOrFn<TOnDynamic>,
    UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>,
    UnwrapFormAsyncValidateOrFn<TOnServer>
  >
  /**
   * An internal mechanism used for keeping track of validation logic in a form.
   */
  validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
  /**
   * A record of field metadata for each field in the form, not including the derived properties, like `errors` and such
   */
  fieldMetaBase: Partial<Record<DeepKeys<TFormData>, AnyFieldMetaBase>>
  /**
   * 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 DerivedFormState<
  in out TFormData,
  in out TOnMount extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> = {
  /**
   * 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<
    | UnwrapFormValidateOrFn<TOnMount>
    | UnwrapFormValidateOrFn<TOnChange>
    | UnwrapFormAsyncValidateOrFn<TOnChangeAsync>
    | UnwrapFormValidateOrFn<TOnBlur>
    | UnwrapFormAsyncValidateOrFn<TOnBlurAsync>
    | UnwrapFormValidateOrFn<TOnSubmit>
    | UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>
    | UnwrapFormValidateOrFn<TOnDynamic>
    | UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>
    | UnwrapFormAsyncValidateOrFn<TOnServer>
  >
  /**
   * 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<Record<DeepKeys<TFormData>, AnyFieldMeta>>
}

export interface FormState<
  in out TFormData,
  in out TOnMount extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
>
  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<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
>(
  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),
    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
>

/**
 * 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<TFormData>,
  in out TOnChange extends undefined | FormValidateOrFn<TFormData>,
  in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  in out TSubmitMeta = never,
> implements FieldManipulator<TFormData, TSubmitMeta> {
  /**
   * 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: Derived<
    FormState<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer
    >['fieldMeta']
  >
  store: Derived<
    FormState<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer
    >
  >
  /**
   * A record of field information for each field in the form.
   */
  fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>> = {} as any

  get state() {
    return this.store.state
  }

  /**
   * @private
   */
  prevTransformArray: unknown[] = []

  /**
   * @private
   */
  timeoutIds: {
    validations: Record<ValidationCause, ReturnType<typeof setTimeout> | null>
    listeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
    formListeners: Record<ListenerCause, ReturnType<typeof setTimeout> | 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<ValidationCause, never>,
      listeners: {} as Record<ListenerCause, never>,
      formListeners: {} as Record<ListenerCause, never>,
    }

    this._formId = opts?.formId ?? uuid()

    this._devtoolsSubmissionOverride = false

    this.baseStore = new Store(
      getDefaultFormState({
        ...(opts?.defaultState as any),
        values: opts?.defaultValues ?? opts?.defaultState?.values,
        isFormValid: true,
      }),
    )

    this.fieldMetaDerived = new Derived({
      deps: [this.baseStore],
      fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => {
        const prevVal = _prevVal as
          | Record<DeepKeys<TFormData>, AnyFieldMeta>
          | undefined
        const prevBaseStore = prevDepVals?.[0]
        const currBaseStore = currDepVals[0]

        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<keyof typeof currBaseStore.fieldMetaBase>) {
          const currBaseMeta = currBaseStore.fieldMetaBase[
            fieldName as never
          ] as AnyFieldMetaBase

          const prevBaseMeta = prevBaseStore?.fieldMetaBase[
            fieldName as never
          ] as AnyFieldMetaBase | 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,
              getBy(this.options.defaultValues, fieldName),
            ) ||
            evaluate(
              curFieldVal,
              // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
              this.getFieldInfo(fieldName)?.instance?.options.defaultValue,
            )

          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 AnyFieldMeta as AnyFieldMeta
        }

        if (!Object.keys(currBaseStore.fieldMetaBase).length) return fieldMeta

        if (
          prevVal &&
          originalMetaCount === Object.keys(currBaseStore.fieldMetaBase).length
        ) {
          return prevVal
        }

        return fieldMeta
      },
    })

    this.store = new Derived({
      deps: [this.baseStore, this.fieldMetaDerived],
      fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => {
        const prevVal = _prevVal as
          | FormState<
              TFormData,
              TOnMount,
              TOnChange,
              TOnChangeAsync,
              TOnBlur,
              TOnBlurAsync,
              TOnSubmit,
              TOnSubmitAsync,
              TOnDynamic,
              TOnDynamicAsync,
              TOnServer
            >
          | undefined
        const prevBaseStore = prevDepVals?.[0]
        const currBaseStore = currDepVals[0]
        const currFieldMeta = currDepVals[1]

        // Computed state
        const fieldMetaValues = Object.values(currFieldMeta).filter(
          Boolean,
        ) as AnyFieldMeta[]

        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 (
          !prevBaseStore ||
          currBaseStore.errorMap !== prevBaseStore.errorMap
        ) {
          errors = Object.values(currBaseStore.errorMap).reduce<
            Array<
              | UnwrapFormValidateOrFn<TOnMount>
              | UnwrapFormValidateOrFn<TOnChange>
              | UnwrapFormAsyncValidateOrFn<TOnChangeAsync>
              | UnwrapFormValidateOrFn<TOnBlur>
              | UnwrapFormAsyncValidateOrFn<TOnBlurAsync>
              | UnwrapFormValidateOrFn<TOnSubmit>
              | UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>
              | UnwrapFormAsyncValidateOrFn<TOnServer>
            >
          >((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 &&
          prevBaseStore &&
          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(prevBaseStore, currBaseStore)
        ) {
          return prevVal
        }

        let 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
        >

        // Only run transform if state has shallowly changed - IE how React.useEffect works
        const transformArray = this.options.transform?.deps ?? []
        const shouldTransform =
          transformArray.length !== this.prevTransformArray.length ||
          transformArray.some((val, i) => val !== this.prevTransformArray[i])

        if (shouldTransform) {
          const newObj = Object.assign({}, this, { state })
          // This mutates the state
          this.options.transform?.fn(newObj)
          state = newObj.state
          this.prevTransformArray = transformArray
        }

        return state
      },
    })

    this.handleSubmit = this.handleSubmit.bind(this)

    this.update(opts || {})

    // devtool broadcasts
    this.store.subscribe(() => {
      throttleFormState(this)
    })

    // devtool requests
    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,
        })
      }
    })

    formEventClient.on('request-form-reset', (e) => {
      if (e.payload.id === this._formId) {
        this.reset()
      }
    })

    formEventClient.on('request-form-force-submit', (e) => {
      if (e.payload.id === this._formId) {
        this._devtoolsSubmissionOverride = true
        this.handleSubmit()
        this._devtoolsSubmissionOverride = false
      }
    })
  }

  get formId(): string {
    return this._formId
  }

  /**
   * @private
   */
  runValidator<
    TValue extends TStandardSchemaValidatorValue<TFormData> & {
      formApi: AnyFormApi
    },
    TType extends 'validate' | 'validateAsync',
  >(props: {
    validate: TType extends 'validate'
      ? FormValidateOrFn<TFormData>
      : FormAsyncValidateOrFn<TFormData>
    value: TValue
    type: TType
  }): unknown {
    if (isStandardSchemaValidator(props.validate)) {
      return standardSchemaValidators[props.type](
        props.value,
        props.validate,
      ) as never
    }

    return (props.validate as FormValidateFn<any>)(props.value) as never
  }

  mount = () => {
    const cleanupFieldMetaDerived = this.fieldMetaDerived.mount()
    const cleanupStoreDerived = this.store.mount()
    const cleanup = () => {
      cleanupFieldMetaDerived()
      cleanupStoreDerived()

      // 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

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    const shouldUpdateReeval = !!options.transform?.deps?.some(
      (val, i) => val !== this.prevTransformArray[i],
    )

    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 && !shouldUpdateReeval) return

    batch(() => {
      this.baseStore.setState(() =>
        getDefaultFormState(
          Object.assign(
            {},
            this.state as any,

            shouldUpdateState ? options.defaultState : {},

            shouldUpdateValues
              ? {
                  values: options.defaultValues,
                }
              : {},

            shouldUpdateReeval
              ? { _force_re_eval: !this.state._force_re_eval }
              : {},
          ),
        ),
      )
    })

    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(() =>
      getDefaultFormState({
        ...(this.options.defaultState as any),
        values:
          values ??
          this.options.defaultValues ??
          this.options.defaultState?.values,
        fieldMetaBase,
      }),
    )
  }

  /**
   * Validates all fields using the correct handlers for a given validation cause.
   */
  validateAllFields = async (cause: ValidationCause) => {
    const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
    batch(() => {
      void (Object.values(this.fieldInfo) as FieldInfo<any>[]).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 }),
            ),
          )
          // If any fields are not touched
          if (!field.instance.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<TFormData, any[]>,
  >(
    field: TField,
    index: number,
    cause: ValidationCause,
  ) => {
    const currentValue = this.getFieldValue(field)

    const lastIndex = Array.isArray(currentValue)
      ? Math.max((currentValue as Array<unknown>).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<TFormData>[]

    // Validate the fields
    const fieldValidationPromises: Promise<ValidationError[]>[] = [] 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 = <TField extends DeepKeys<TFormData>>(
    field: TField,
    cause: ValidationCause,
  ) => {
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    const fieldInstance = this.fieldInfo[field]?.instance
    if (!fieldInstance) return []

    // If the field is not touched (same logic as in validateAllFields)
    if (!fieldInstance.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,
  ): {
    hasErrored: boolean
    fieldsErrorMap: FormErrorMapFromValidator<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync
    >
  } => {
    const validates = getSyncValidatorArray(cause, {
      ...this.options,
      form: this,
      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<TFormData>(rawError)

        const errorMapKey = getErrorMapKey(validateObj.cause)

        for (const field of Object.keys(
          this.state.fieldMeta,
        ) as DeepKeys<TFormData>[]) {
          if (this.baseStore.state.fieldMetaBase[field] === undefined) {
            continue
          }

          const fieldMeta = this.getFieldMeta(field)
          if (!fieldMeta) continue

          const {
            errorMap: currentErrorMap,
            errorSourceMap: currentErrorMapSource,
          } = fieldMeta

          const newFormValidatorError = fieldErrors?.[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],
            })

          if (newSource === 'form') {
            currentValidationErrorMap[field] = {
              ...currentValidationErrorMap[field],
              [errorMapKey]: newFormValidatorError,
            }
          }

          if (
            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
            currentErrorMap?.[errorMapKey] !== newErrorValue
          ) {
            this.setFieldMeta(field, (prev) => ({
              ...prev,
              errorMap: {
                ...prev.errorMap,
                [errorMapKey]: newErrorValue,
              },
              errorSourceMap: {
                ...prev.errorSourceMap,
                [errorMapKey]: newSource,
              },
            }))
          }
        }

        // 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
        }
      }

      /**
       *  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,
  ): Promise<
    FormErrorMapFromValidator<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync
    >
  > => {
    const validates = getAsyncValidatorArray(cause, {
      ...this.options,
      form: this,
      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<ValidationPromiseResult<TFormData>>[] = []

    let fieldErrorsFromFormValidators:
      | Partial<Record<DeepKeys<TFormData>, 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<ValidationPromiseResult<TFormData>>(async (resolve) => {
          let rawError!:
            | ValidationError
            | FormValidationError<unknown>
            | 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<TFormData>(rawError)

          if (fieldErrorsFromNormalizeError) {
            fieldErrorsFromFormValidators = fieldErrorsFromFormValidators
              ? {
                  ...fieldErrorsFromFormValidators,
                  ...fieldErrorsFromNormalizeError,
                }
              : fieldErrorsFromNormalizeError
          }
          const errorMapKey = getErrorMapKey(validateObj.cause)

          for (const field of Object.keys(
            this.state.fieldMeta,
          ) as DeepKeys<TFormData>[]) {
            if (this.baseStore.state.fieldMetaBase[field] === undefined) {
              continue
            }

            const fieldMeta = this.getFieldMeta(field)
            if (!fieldMeta) continue

            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],
              })

            if (
              // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
              currentErrorMap?.[errorMapKey] !== newErrorValue
            ) {
              this.setFieldMeta(field, (prev) => ({
                ...prev,
                errorMap: {
                  ...prev.errorMap,
                  [errorMapKey]: newErrorValue,
                },
                errorSourceMap: {
                  ...prev.errorSourceMap,
                  [errorMapKey]: newSource,
                },
              }))
            }
          }

          this.baseStore.setState((prev) => ({
            ...prev,
            errorMap: {
              ...prev.errorMap,
              [errorMapKey]: formError,
            },
          }))

          resolve(
            fieldErrorsFromFormValidators
              ? { fieldErrors: fieldErrorsFromFormValidators, errorMapKey }
              : undefined,
          )
        }),
      )
    }

    let results: ValidationPromiseResult<TFormData>[] = []

    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<TFormData>] || {}
            const newErrorMap = {
              ...oldErrorMap,
              [errorMapKey]: fieldError,
            }
            fieldsErrorMap[field as DeepKeys<TFormData>] = newErrorMap
          }
        }
      }
    }

    this.baseStore.setState((prev) => ({
      ...prev,
      isFormValidating: false,
    }))

    return fieldsErrorMap
  }

  /**
   * @private
   */
  validate = (
    cause: ValidationCause,
  ):
    | 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)

    if (hasErrored && !this.options.asyncAlways) {
      return fieldsErrorMap
    }

    // No error? Attempt async validation
    return this.validateAsync(cause)
  }

  // Needs to edgecase in the React adapter specifically to avoid type errors
  handleSubmit(): Promise<void>
  handleSubmit(submitMeta: TSubmitMeta): Promise<void>
  handleSubmit(submitMeta?: TSubmitMeta): Promise<void> {
    return this._handleSubmit(submitMeta)
  }

  /**
   * Handles the form submission, performs validation, and calls the appropriate onSubmit or onSubmitInvalid callbacks.
   */
  _handleSubmit = async (submitMeta?: TSubmitMeta): Promise<void> => {
    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<any>[]).forEach(
        (field) => {
          if (!field.instance) return
          // If any fields are not touched
          if (!field.instance.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) {
      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 AnyFieldMeta[])
          .map((meta: AnyFieldMeta) => 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<TFormData>[]).forEach(
        (field) => {
          field.instance?.options.listeners?.onSubmit?.({
            value: field.instance.state.value,
            fieldApi: field.instance,
          })
        },
      )
    })

    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 = <TField extends DeepKeys<TFormData>>(
    field: TField,
  ): DeepValue<TFormData, TField> => getBy(this.state.values, field)

  /**
   * Gets the metadata of the specified field.
   */
  getFieldMeta = <TField extends DeepKeys<TFormData>>(
    field: TField,
  ): AnyFieldMeta | undefined => {
    return this.state.fieldMeta[field]
  }

  /**
   * Gets the field info of the specified field.
   */
  getFieldInfo = <TField extends DeepKeys<TFormData>>(
    field: TField,
  ): FieldInfo<TFormData> => {
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    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 = <TField extends DeepKeys<TFormData>>(
    field: TField,
    updater: Updater<AnyFieldMetaBase>,
  ) => {
    this.baseStore.setState((prev) => {
      return {
        ...prev,
        fieldMetaBase: {
          ...prev.fieldMetaBase,
          [field]: functionalUpdate(
            updater,
            prev.fieldMetaBase[field] as never,
          ),
        },
      }
    })
  }

  /**
   * resets every field's meta
   */
  resetFieldMeta = <TField extends DeepKeys<TFormData>>(
    fieldMeta: Partial<Record<TField, AnyFieldMeta>>,
  ): Partial<Record<TField, AnyFieldMeta>> => {
    return Object.keys(fieldMeta).reduce(
      (acc, key) => {
        const fieldKey = key as TField
        acc[fieldKey] = defaultFieldMeta
        return acc
      },
      {} as Partial<Record<TField, AnyFieldMeta>>,
    )
  }

  /**
   * Sets the value of the specified field and optionally updates the touched state.
   */
  setFieldValue = <TField extends DeepKeys<TFormData>>(
    field: TField,
    updater: Updater<DeepValue<TFormData, TField>>,
    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 = <TField extends DeepKeys<TFormData>>(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 = <TField extends DeepKeysOfType<TFormData, any[]>>(
    field: TField,
    value: DeepValue<TFormData, TField> extends any[]
      ? DeepValue<TFormData, TField>[number]
      : never,
    options?: UpdateMetaOptions,
  ) => {
    this.setFieldValue(
      field,
      (prev) => [...(Array.isArray(prev) ? prev : []), value] as any,
      options,
    )
  }

  insertFieldValue = async <TField extends DeepKeysOfType<TFormData, any[]>>(
    field: TField,
    index: number,
    value: DeepValue<TFormData, TField> extends any[]
      ? DeepValue<TFormData, TField>[number]
      : never,
    options?: UpdateMetaOptions,
  ) => {
    this.setFieldValue(
      field,
      (prev) => {
        return [
          ...(prev as DeepValue<TFormData, TField>[]).slice(0, index),
          value,
          ...(prev as DeepValue<TFormData, TField>[]).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 <TField extends DeepKeysOfType<TFormData, any[]>>(
    field: TField,
    index: number,
    value: DeepValue<TFormData, TField> extends any[]
      ? DeepValue<TFormData, TField>[number]
      : never,
    options?: UpdateMetaOptions,
  ) => {
    this.setFieldValue(
      field,
      (prev) => {
        return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
          i === index ? value : d,
        ) 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')
      await this.validateArrayFieldsStartingFrom(field, index, 'change')
    }
  }

  /**
   * Removes a value from an array field at the specified index.
   */
  removeFieldValue = async <TField extends DeepKeysOfType<TFormData, any[]>>(
    field: TField,
    index: number,
    options?: UpdateMetaOptions,
  ) => {
    const fieldValue = this.getFieldValue(field)

    const lastIndex = Array.isArray(fieldValue)
      ? Math.max((fieldValue as Array<unknown>).length - 1, 0)
      : null

    this.setFieldValue(
      field,
      (prev) => {
        return (prev as DeepValue<TFormData, TField>[]).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 = <TField extends DeepKeysOfType<TFormData, any[]>>(
    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<TFormData>, 'change')
      this.validateField(`${field}[${index2}]` as DeepKeys<TFormData>, 'change')
    }
  }

  /**
   * Moves the value at the first specified index to the second specified index within an array field.
   */
  moveFieldValues = <TField extends DeepKeysOfType<TFormData, any[]>>(
    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<TFormData>, 'change')
      this.validateField(`${field}[${index2}]` as DeepKeys<TFormData>, 'change')
    }
  }

  /**
   * Clear all values within an array field.
   */
  clearFieldValues = <TField extends DeepKeysOfType<TFormData, any[]>>(
    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 }),
    )

    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 = <TField extends DeepKeys<TFormData>>(field: TField) => {
    this.baseStore.setState((prev) => {
      return {
        ...prev,
        fieldMetaBase: {
          ...prev.fieldMetaBase,
          [field]: defaultFieldMeta,
        },
        values: this.options.defaultValues
          ? setBy(prev.values, field, getBy(this.options.defaultValues, field))
          : prev.values,
      }
    })
  }

  /**
   * Updates the form's errorMap
   */
  setErrorMap = (
    errorMap: FormValidationErrorMap<
      TFormData,
      UnwrapFormValidateOrFn<TOnMount>,
      UnwrapFormValidateOrFn<TOnChange>,
      UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
      UnwrapFormValidateOrFn<TOnBlur>,
      UnwrapFormAsyncValidateOrFn<TOnBlurAsync>,
      UnwrapFormValidateOrFn<TOnSubmit>,
      UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>,
      UnwrapFormValidateOrFn<TOnDynamic>,
      UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>,
      UnwrapFormAsyncValidateOrFn<TOnServer>
    >,
  ) => {
    batch(() => {
      Object.entries(errorMap).forEach(([key, value]) => {
        const errorMapKey = key as ValidationErrorMapKeys

        if (isGlobalFormValidationError(value)) {
          const { formError, fieldErrors } = normalizeError<TFormData>(value)

          for (const fieldName of Object.keys(
            this.fieldInfo,
          ) as DeepKeys<TFormData>[]) {
            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<
        | UnwrapFormValidateOrFn<TOnMount>
        | UnwrapFormValidateOrFn<TOnChange>
        | UnwrapFormAsyncValidateOrFn<TOnChangeAsync>
        | UnwrapFormValidateOrFn<TOnBlur>
        | UnwrapFormAsyncValidateOrFn<TOnBlurAsync>
        | UnwrapFormValidateOrFn<TOnSubmit>
        | UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>
        | UnwrapFormValidateOrFn<TOnDynamic>
        | UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>
        | UnwrapFormAsyncValidateOrFn<TOnServer>
      >
      errorMap: ValidationErrorMap<
        UnwrapFormValidateOrFn<TOnMount>,
        UnwrapFormValidateOrFn<TOnChange>,
        UnwrapFormAsyncValidateOrFn<TOnChangeAsync>,
        UnwrapFormValidateOrFn<TOnBlur>,
        UnwrapFormAsyncValidateOrFn<TOnBlurAsync>,
        UnwrapFormValidateOrFn<TOnSubmit>,
        UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>,
        UnwrapFormValidateOrFn<TOnDynamic>,
        UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>,
        UnwrapFormAsyncValidateOrFn<TOnServer>
      >
    }
    fields: Record<
      DeepKeys<TFormData>,
      { 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 AnyFieldMeta).length &&
            (fieldMeta as AnyFieldMeta).errors.length
          ) {
            acc[fieldName as DeepKeys<TFormData>] = {
              errors: (fieldMeta as AnyFieldMeta).errors,
              errorMap: (fieldMeta as AnyFieldMeta).errorMap,
            }
          }

          return acc
        },
        {} as Record<
          DeepKeys<TFormData>,
          { 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<TFormData, unknown>) => {
    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<TFormData, unknown>,
  ) => {
    return standardSchemaValidators.validateAsync(
      { value: this.state.values, validationSource: 'form' },
      schema,
    )
  }
}

function normalizeError<TFormData>(rawError?: FormValidationError<unknown>): {
  formError: ValidationError
  fieldErrors?: Partial<Record<DeepKeys<TFormData>, 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'
  }
}


--- packages/form-core/tests/FormApi.spec.ts ---
import { describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import { FieldApi, FormApi, formEventClient } from '../src/index'
import { sleep } from './utils'
import type { AnyFieldApi, AnyFormApi } from '../src/index'

describe('form api', () => {
  it('should get default form state when default values are passed', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    expect(form.state).toMatchObject({
      values: {
        name: 'test',
      },
    })
  })

  it('should get default form state when default state is passed', () => {
    const form = new FormApi({
      defaultState: {
        submissionAttempts: 30,
      },
    })
    form.mount()
    expect(form.state).toMatchObject({
      submissionAttempts: 30,
    })
  })

  it('should handle updating form state', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    form.update({
      defaultValues: {
        name: 'other',
      },
      defaultState: {
        submissionAttempts: 300,
      },
    })

    expect(form.state).toMatchObject({
      values: {
        name: 'other',
      },
      submissionAttempts: 300,
    })
  })

  it('should reset the form state properly', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    form.setFieldValue('name', 'other', {
      dontUpdateMeta: true,
    })
    form.state.submissionAttempts = 300

    form.reset()

    expect(form.state).toMatchObject({
      values: {
        name: 'test',
      },
      fieldMeta: {},
      submissionAttempts: 0,
    })
  })

  it('should reset with provided custom default values', () => {
    const defaultValues = {
      name: 'test',
      surname: 'test2',
      age: 30,
    }
    const form = new FormApi({
      defaultValues: defaultValues,
    })

    form.mount()
    form.setFieldValue('name', 'other')

    expect(form.state.values).toEqual({
      name: 'other',
      surname: 'test2',
      age: 30,
    })

    form.reset()

    expect(form.state.values).toEqual(defaultValues)

    form.setFieldValue('name', 'other2')
    form.setFieldValue('age', 33)

    const resetValues = {
      name: 'reset name',
      age: 40,
      surname: 'reset surname',
    }
    form.reset(resetValues)

    expect(form.state).toMatchObject({
      values: {
        name: 'reset name',
        age: 40,
        surname: 'reset surname',
      },
    })
  })

  it('form should reset default value when resetting in onSubmit', async () => {
    const defaultValues = {
      name: '',
    }
    const form = new FormApi({
      defaultValues: defaultValues,
      onSubmit: ({ value }) => {
        form.reset(value)

        expect(form.options.defaultValues).toMatchObject({
          name: 'test',
        })
      },
    })
    form.mount()
    form.setFieldValue('name', 'test')
    form.handleSubmit()
  })

  it('should reset and set the new default values that are restored after an empty reset', () => {
    const form = new FormApi({ defaultValues: { name: 'initial' } })
    form.mount()

    const field = new FieldApi({ form, name: 'name' })
    field.mount()

    form.reset({ name: 'other' })
    expect(form.state.values).toEqual({ name: 'other' })

    field.handleChange('')
    form.reset()
    expect(form.state.values).toEqual({ name: 'other' })
  })

  it('should reset without setting the new default values that are restored after an empty reset if opts.keepDefaultValues is true', () => {
    const form = new FormApi({ defaultValues: { name: 'initial' } })
    form.mount()

    const field = new FieldApi({ form, name: 'name' })
    field.mount()

    form.reset({ name: 'other' }, { keepDefaultValues: true })
    expect(form.state.values).toEqual({ name: 'other' })

    field.handleChange('')
    form.reset()
    expect(form.state.values).toEqual({ name: 'initial' })
  })

  it('should handle multiple fields with mixed mount states', () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
        email: '',
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })

    firstNameField.mount()

    expect(form.state.fieldMeta.firstName).toBeDefined()

    expect(form.state.fieldMeta.email).toBeUndefined()

    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })
    lastNameField.mount()

    expect(form.state.fieldMeta.lastName).toBeDefined()
  })

  it("should get a field's value", () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    expect(form.getFieldValue('name')).toEqual('test')
  })

  it("should set a field's value", () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    form.setFieldValue('name', 'other')

    expect(form.getFieldValue('name')).toEqual('other')
  })

  it("should be dirty after a field's value has been set", () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()

    form.setFieldValue('name', 'other')

    expect(form.state.isDirty).toBe(true)
    expect(form.state.isPristine).toBe(false)
  })

  it('should be clean again after being reset from a dirty state', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()

    form.setFieldMeta('name', (meta) => ({
      ...meta,
      isDirty: true,
      isPristine: false,
    }))

    form.reset()

    expect(form.state.isDirty).toBe(false)
    expect(form.state.isPristine).toBe(true)
  })

  it("should push an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
    })
    form.mount()
    form.pushFieldValue('names', 'other')

    expect(form.getFieldValue('names')).toStrictEqual(['test', 'other'])
  })

  it("should insert an array field's value as first element", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.insertFieldValue('names', 0, 'other')

    expect(form.getFieldValue('names')).toStrictEqual([
      'other',
      'one',
      'two',
      'three',
    ])
  })

  it("should run onChange validation when pushing an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    const field = new FieldApi({ form, name: 'names' })
    field.mount()

    form.pushFieldValue('names', 'other')

    expect(form.state.errors).toStrictEqual(['At least 3 names are required'])
  })

  it("should insert an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.insertFieldValue('names', 1, 'other')

    expect(form.getFieldValue('names')).toStrictEqual([
      'one',
      'other',
      'two',
      'three',
    ])
  })

  it("should insert an array field's value at the end if the index is higher than the length", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.insertFieldValue('names', 10, 'other')

    expect(form.getFieldValue('names')).toStrictEqual([
      'one',
      'two',
      'three',
      'other',
    ])
  })

  it("should replace an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.replaceFieldValue('names', 1, 'other')

    expect(form.getFieldValue('names')).toStrictEqual(['one', 'other', 'three'])
  })

  it("should do nothing when replacing an array field's value with an index that doesn't exist", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.replaceFieldValue('names', 10, 'other')

    expect(form.getFieldValue('names')).toStrictEqual(['one', 'two', 'three'])
  })

  it("should run onChange validation when inserting an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    form.insertFieldValue('names', 1, 'other')

    expect(form.state.errors).toStrictEqual(['At least 3 names are required'])
  })

  it("should shift meta (including errors) when inserting an array field's primitive value", async () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()

    new FieldApi({ form, name: 'names' }).mount()

    const field0 = new FieldApi({ form, name: 'names[0]' })
    field0.mount()

    const field1 = new FieldApi({
      form,
      name: 'names[1]',
      validators: {
        onChange: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field1.mount()

    const field2 = new FieldApi({
      form,
      name: 'names[2]',
      validators: {
        onChange: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field2.mount()

    const field3 = new FieldApi({ form, name: 'names[3]' })
    field3.mount()

    field0.handleChange('foo')
    expect(field0.state.meta.isDirty).toBe(true)

    field1.handleBlur()
    expect(field1.state.meta.isBlurred).toBe(true)

    field1.handleChange('other')
    expect(field1.state.meta.errors).toStrictEqual(['Invalid value'])

    await form.insertFieldValue('names', 1, 'test')

    // field0 meta didn't move on field1 as we inserted after it
    expect(field0.state.meta.isDirty).toBe(true)
    expect(field1.state.meta.isDirty).toBe(false)

    // field1 meta (isBlurred = true) moved on field2
    expect(field0.state.meta.isBlurred).toBe(false)
    expect(field1.state.meta.isBlurred).toBe(false)
    expect(field2.state.meta.isBlurred).toBe(true)
    expect(field3.state.meta.isBlurred).toBe(false)

    // field1 errors moved on field2
    expect(field0.state.meta.errors).toStrictEqual([])
    expect(field1.state.meta.errors).toStrictEqual([])
    expect(field2.state.meta.errors).toStrictEqual(['Invalid value'])
    expect(field3.state.meta.errors).toStrictEqual([])
  })

  it("should shift meta (including errors) when inserting an array field's primitive value, last element meta", async () => {
    const form = new FormApi({ defaultValues: { names: ['one'] } })
    form.mount()

    new FieldApi({ form, name: 'names' }).mount()

    const field0 = new FieldApi({
      form,
      name: 'names[0]',
      validators: {
        onBlur: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field0.mount()

    const field1 = new FieldApi({ form, name: 'names[1]' })
    field1.mount()

    field0.handleChange('foo')
    expect(field0.state.meta.isDirty).toBe(true)

    field0.handleBlur()
    expect(field0.state.meta.isBlurred).toBe(true)
    expect(field0.state.meta.errors).toStrictEqual(['Invalid value'])

    await form.insertFieldValue('names', 0, 'test')

    expect(field0.state.meta.isDirty).toBe(false)
    expect(field0.state.meta.isBlurred).toBe(false)
    expect(field0.state.meta.errors).toStrictEqual([])

    expect(field1.state.meta.isDirty).toBe(true)
    expect(field1.state.meta.isBlurred).toBe(true)
    expect(field1.state.meta.errors).toStrictEqual(['Invalid value'])
  })

  it("should shift meta (including errors) when inserting an array field's nested value", async () => {
    const form = new FormApi({
      defaultValues: {
        names: [{ first: 'test' }, { first: 'test2' }],
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    const field0 = new FieldApi({
      form,
      name: 'names[0].first',
      validators: {
        onBlur: ({ value }) => value !== 'test' && 'Invalid value',
      },
      defaultValue: 'other',
    })
    field0.mount()

    const field1 = new FieldApi({
      form,
      name: 'names[1].first',
      validators: {
        onBlur: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field1.mount()

    expect(field0.state.meta.errors).toStrictEqual([])
    expect(field0.state.meta.isBlurred).toBe(false)
    field0.handleBlur()
    expect(field0.state.meta.errors).toStrictEqual(['Invalid value'])
    expect(field0.state.meta.isBlurred).toBe(true)

    await form.insertFieldValue('names', 0, { first: 'test' })

    expect(field0.state.meta.errors).toStrictEqual([])
    expect(field0.state.meta.isBlurred).toBe(false)
    expect(field1.state.meta.errors).toStrictEqual(['Invalid value'])
    expect(field1.state.meta.isBlurred).toBe(true)
  })

  it("should validate all shifted fields when inserting an array field's value", async () => {
    const form = new FormApi({
      defaultValues: {
        names: [{ first: 'test' }, { first: 'test2' }],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    const field1 = new FieldApi({
      form,
      name: 'names[0].first',
      defaultValue: 'test',
      validators: {
        onChange: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field1.mount()

    expect(field1.state.meta.errors).toStrictEqual([])

    await form.replaceFieldValue('names', 0, { first: 'other' })

    expect(field1.state.meta.errors).toStrictEqual(['Invalid value'])
  })

  it("should remove an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.removeFieldValue('names', 1)

    expect(form.getFieldValue('names')).toStrictEqual(['one', 'three'])
  })

  it("should run onChange validation when removing an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 1 ? undefined : 'At least 1 name is required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    form.removeFieldValue('names', 0)

    expect(form.state.errors).toStrictEqual(['At least 1 name is required'])
  })

  it("should validate following fields when removing an array field's value", async () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2', 'test3'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 1 ? undefined : 'At least 1 name is required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    const field1 = new FieldApi({
      form,
      name: 'names[0]',
      defaultValue: 'test',
      validators: {
        onChange: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field1.mount()
    const field2 = new FieldApi({
      form,
      name: 'names[1]',
      defaultValue: 'test2',
      validators: {
        onChange: ({ value }) => value !== 'test2' && 'Invalid value',
      },
    })
    field2.mount()
    const field3 = new FieldApi({
      form,
      name: 'names[2]',
      defaultValue: 'test3',
      validators: {
        onChange: ({ value }) => value !== 'test3' && 'Invalid value',
      },
    })
    field3.mount()

    expect(field1.state.meta.errors).toStrictEqual([])
    expect(field2.state.meta.errors).toStrictEqual([])
    expect(field3.state.meta.errors).toStrictEqual([])

    await form.removeFieldValue('names', 1)

    expect(field1.state.meta.errors).toStrictEqual([])
    expect(field2.state.meta.errors).toStrictEqual(['Invalid value'])
    // This field does not exist anymore. Therefore, its validation should also not run
    expect(field3.state.meta.errors).toStrictEqual([])
  })

  it('should shift meta (nested) when removing array values', async () => {
    const form = new FormApi({
      defaultValues: {
        users: [
          { name: 'test', surname: 'test' },
          { name: 'test2', surname: 'test2' },
          { name: 'test3', surname: 'test3' },
        ],
      },
    })
    form.mount()
    new FieldApi({ form, name: 'users' }).mount()
    const field0Name = new FieldApi({ form, name: 'users[0].name' })
    field0Name.mount()
    const field0Surname = new FieldApi({ form, name: 'users[0].surname' })
    field0Surname.mount()
    const field1Name = new FieldApi({ form, name: 'users[1].name' })
    field1Name.mount()
    const field1Surname = new FieldApi({ form, name: 'users[1].surname' })
    field1Surname.mount()
    const field2Name = new FieldApi({ form, name: 'users[2].name' })
    field2Name.mount()
    const field2Surname = new FieldApi({ form, name: 'users[2].surname' })
    field2Surname.mount()

    expect(field0Name.state.meta.isBlurred).toBe(false)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(false)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
    expect(field2Name.state.meta.isBlurred).toBe(false)
    expect(field2Surname.state.meta.isBlurred).toBe(false)

    field0Name.handleBlur()
    field2Name.handleBlur()
    field2Surname.handleBlur()

    expect(field0Name.state.meta.isBlurred).toBe(true)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(false)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
    expect(field2Name.state.meta.isBlurred).toBe(true)
    expect(field2Surname.state.meta.isBlurred).toBe(true)

    await form.removeFieldValue('users', 1)

    expect(field0Name.state.meta.isBlurred).toBe(true)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    // field2's meta moved on field1 now
    expect(field1Name.state.meta.isBlurred).toBe(true)
    expect(field1Surname.state.meta.isBlurred).toBe(true)
  })

  it("should swap an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    form.swapFieldValues('names', 1, 2)

    expect(form.getFieldValue('names')).toStrictEqual(['one', 'three', 'two'])
  })

  it("should run onChange validation when swapping an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()
    expect(form.state.errors).toStrictEqual([])

    form.swapFieldValues('names', 1, 2)

    expect(form.state.errors).toStrictEqual(['At least 3 names are required'])
  })

  it('should run validation on swapped fields', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    const field1 = new FieldApi({
      form,
      name: 'names[0]',
      defaultValue: 'test',
      validators: {
        onChange: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field1.mount()

    const field2 = new FieldApi({
      form,
      name: 'names[1]',
      defaultValue: 'test2',
    })
    field2.mount()

    expect(field1.state.meta.errors).toStrictEqual([])
    expect(field2.state.meta.errors).toStrictEqual([])

    form.swapFieldValues('names', 0, 1)

    expect(field1.state.meta.errors).toStrictEqual(['Invalid value'])
    expect(field2.state.meta.errors).toStrictEqual([])
  })

  it('should swap meta when swapping array values', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    const field0 = new FieldApi({ form, name: 'names[0]' })
    field0.mount()

    const field1 = new FieldApi({ form, name: 'names[1]' })
    field1.mount()

    const field2 = new FieldApi({ form, name: 'names[2]' })
    field2.mount()

    field1.handleBlur()
    field1.setErrorMap({ onSubmit: 'test' as never })

    expect(field0.state.meta.isBlurred).toBe(false)
    expect(field1.state.meta.isBlurred).toBe(true)
    expect(field2.state.meta.isBlurred).toBe(false)

    expect(field0.state.meta.errors).toStrictEqual([])
    expect(field1.state.meta.errors).toStrictEqual(['test'])
    expect(field2.state.meta.errors).toStrictEqual([])

    form.swapFieldValues('names', 1, 2)

    expect(form.getFieldValue('names')).toStrictEqual(['one', 'three', 'two'])

    expect(field0.state.meta.isBlurred).toBe(false)
    expect(field1.state.meta.isBlurred).toBe(false)
    expect(field2.state.meta.isBlurred).toBe(true)

    expect(field0.state.meta.errors).toStrictEqual([])
    expect(field1.state.meta.errors).toStrictEqual([])
    // field2 doesn't have any error since it has been revalidated after swap (but has no validator)
    expect(field2.state.meta.errors).toStrictEqual([])
  })

  it('should swap meta (nested) when swapping array values', () => {
    const form = new FormApi({
      defaultValues: {
        users: [
          { name: 'test', surname: 'test' },
          { name: 'test2', surname: 'test2' },
        ],
      },
    })
    form.mount()
    new FieldApi({ form, name: 'users' }).mount()

    const field0Name = new FieldApi({ form, name: 'users[0].name' })
    field0Name.mount()

    const field0Surname = new FieldApi({ form, name: 'users[0].surname' })
    field0Surname.mount()

    const field1Name = new FieldApi({ form, name: 'users[1].name' })
    field1Name.mount()

    const field1Surname = new FieldApi({ form, name: 'users[1].surname' })
    field1Surname.mount()

    field0Name.handleBlur()
    expect(field0Name.state.meta.isBlurred).toBe(true)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(false)
    expect(field1Surname.state.meta.isBlurred).toBe(false)

    form.swapFieldValues('users', 0, 1)

    expect(form.getFieldValue('users')).toStrictEqual([
      { name: 'test2', surname: 'test2' },
      { name: 'test', surname: 'test' },
    ])

    expect(field0Name.state.meta.isBlurred).toBe(false)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(true)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
  })

  it("should move an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['one', 'two', 'three'],
      },
    })
    form.mount()
    form.moveFieldValues('names', 1, 2)

    expect(form.getFieldValue('names')).toStrictEqual(['one', 'three', 'two'])
  })

  it("should run onChange validation when moving an array field's value", () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    expect(form.state.errors).toStrictEqual([])
    form.moveFieldValues('names', 0, 1)

    expect(form.state.errors).toStrictEqual(['At least 3 names are required'])
  })

  it('should run validation on moved fields', () => {
    const form = new FormApi({
      defaultValues: {
        names: ['test', 'test2'],
      },
      validators: {
        onChange: ({ value }) =>
          value.names.length > 3 ? undefined : 'At least 3 names are required',
      },
    })
    form.mount()
    // Since validation runs through the field, a field must be mounted for that array
    new FieldApi({ form, name: 'names' }).mount()

    const field1 = new FieldApi({
      form,
      name: 'names[0]',
      defaultValue: 'test',
      validators: {
        onChange: ({ value }) => value !== 'test' && 'Invalid value',
      },
    })
    field1.mount()
    const field2 = new FieldApi({
      form,
      name: 'names[1]',
      defaultValue: 'test2',
    })
    field2.mount()

    expect(field1.state.meta.errors).toStrictEqual([])
    expect(field2.state.meta.errors).toStrictEqual([])

    form.swapFieldValues('names', 0, 1)

    expect(field1.state.meta.errors).toStrictEqual(['Invalid value'])
    expect(field2.state.meta.errors).toStrictEqual([])
  })

  it('should move meta (nested) when moving array values forward', () => {
    const form = new FormApi({
      defaultValues: {
        users: [
          { name: 'test', surname: 'test' },
          { name: 'test2', surname: 'test2' },
          { name: 'test3', surname: 'test3' },
        ],
      },
    })
    form.mount()
    new FieldApi({ form, name: 'users' }).mount()

    const field0Name = new FieldApi({ form, name: 'users[0].name' })
    field0Name.mount()

    const field0Surname = new FieldApi({ form, name: 'users[0].surname' })
    field0Surname.mount()

    const field1Name = new FieldApi({ form, name: 'users[1].name' })
    field1Name.mount()

    const field1Surname = new FieldApi({ form, name: 'users[1].surname' })
    field1Surname.mount()

    const field2Name = new FieldApi({ form, name: 'users[2].name' })
    field2Name.mount()

    const field2Surname = new FieldApi({ form, name: 'users[2].surname' })
    field2Surname.mount()

    field0Name.handleBlur()
    expect(field0Name.state.meta.isBlurred).toBe(true)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(false)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
    expect(field2Name.state.meta.isBlurred).toBe(false)
    expect(field2Surname.state.meta.isBlurred).toBe(false)

    form.moveFieldValues('users', 0, 2)

    expect(form.getFieldValue('users')).toStrictEqual([
      { name: 'test2', surname: 'test2' },
      { name: 'test3', surname: 'test3' },
      { name: 'test', surname: 'test' },
    ])

    expect(field0Name.state.meta.isBlurred).toBe(false)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(false)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
    expect(field2Name.state.meta.isBlurred).toBe(true)
    expect(field2Surname.state.meta.isBlurred).toBe(false)
  })

  it('should move meta (nested) when moving array values backward', () => {
    const form = new FormApi({
      defaultValues: {
        users: [
          { name: 'test', surname: 'test' },
          { name: 'test2', surname: 'test2' },
          { name: 'test3', surname: 'test3' },
        ],
      },
    })
    form.mount()
    new FieldApi({ form, name: 'users' }).mount()

    const field0Name = new FieldApi({ form, name: 'users[0].name' })
    field0Name.mount()

    const field0Surname = new FieldApi({ form, name: 'users[0].surname' })
    field0Surname.mount()

    const field1Name = new FieldApi({ form, name: 'users[1].name' })
    field1Name.mount()

    const field1Surname = new FieldApi({ form, name: 'users[1].surname' })
    field1Surname.mount()

    const field2Name = new FieldApi({ form, name: 'users[2].name' })
    field2Name.mount()

    const field2Surname = new FieldApi({ form, name: 'users[2].surname' })
    field2Surname.mount()

    field1Name.handleBlur()
    field2Name.handleBlur()

    expect(field0Name.state.meta.isBlurred).toBe(false)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(true)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
    expect(field2Name.state.meta.isBlurred).toBe(true)
    expect(field2Surname.state.meta.isBlurred).toBe(false)

    form.moveFieldValues('users', 2, 0)

    expect(form.getFieldValue('users')).toStrictEqual([
      { name: 'test3', surname: 'test3' },
      { name: 'test', surname: 'test' },
      { name: 'test2', surname: 'test2' },
    ])

    expect(field0Name.state.meta.isBlurred).toBe(true)
    expect(field0Surname.state.meta.isBlurred).toBe(false)
    expect(field1Name.state.meta.isBlurred).toBe(false)
    expect(field1Surname.state.meta.isBlurred).toBe(false)
    expect(field2Name.state.meta.isBlurred).toBe(true)
    expect(field2Surname.state.meta.isBlurred).toBe(false)
  })

  it('should preserve array default values when manipulating array values', () => {
    const defaultValues = {
      names: ['one', 'two', 'three'],
    }
    const form = new FormApi({
      defaultValues,
    })
    form.mount()
    form.pushFieldValue('names', 'four')
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)

    form.reset()
    form.insertFieldValue('names', 0, 'other')
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)

    form.reset()
    form.replaceFieldValue('names', 1, 'other')
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)

    form.reset()
    form.removeFieldValue('names', 1)
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)

    form.reset()
    form.swapFieldValues('names', 1, 2)
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)

    form.reset()
    form.moveFieldValues('names', 1, 2)
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)

    form.reset()
    form.clearFieldValues('names')
    expect(form.options.defaultValues?.names).toStrictEqual(defaultValues.names)
  })

  it('should handle fields inside an array', async () => {
    interface Employee {
      firstName: string
    }
    interface Form {
      employees: Partial<Employee>[]
    }

    const form = new FormApi({
      defaultValues: {} as Form,
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'employees',
      defaultValue: [],
    })

    field.mount()

    const fieldInArray = new FieldApi({
      form,
      name: `employees[0].firstName`,
      defaultValue: 'Darcy',
    })
    fieldInArray.mount()
    expect(field.state.value.length).toBe(1)
    expect(fieldInArray.getValue()).toBe('Darcy')
  })

  it('should handle deleting fields in an array', async () => {
    interface Employee {
      firstName: string
    }
    interface Form {
      employees: Partial<Employee>[]
    }

    const form = new FormApi({
      defaultValues: {} as Form,
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'employees',
      defaultValue: [],
    })

    field.mount()

    const fieldInArray = new FieldApi({
      form,
      name: `employees[0].firstName`,
      defaultValue: 'Darcy',
    })
    fieldInArray.mount()
    form.deleteField(`employees[0].firstName`)
    expect(field.state.value.length).toBe(1)
    expect(Object.keys(field.state.value[0]!).length).toBe(0)
  })

  it('should not wipe values when updating', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    form.setFieldValue('name', 'other')

    expect(form.getFieldValue('name')).toEqual('other')

    form.update()

    expect(form.getFieldValue('name')).toEqual('other')
  })

  it('should wipe default values when not touched', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })
    form.mount()
    expect(form.getFieldValue('name')).toEqual('test')

    form.update({
      defaultValues: {
        name: 'other',
      },
    })

    expect(form.getFieldValue('name')).toEqual('other')
  })

  it('should not wipe default values when touched', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'one',
      },
    })
    form.mount()
    expect(form.getFieldValue('name')).toEqual('one')

    form.setFieldValue('name', 'two')

    form.update({
      defaultValues: {
        name: 'three',
      },
    })

    expect(form.getFieldValue('name')).toEqual('two')
  })

  it('should delete field from the form', () => {
    const form = new FormApi({
      defaultValues: {
        names: 'kittu',
        age: 4,
      },
    })
    form.mount()
    form.deleteField('names')

    expect(form.getFieldValue('age')).toStrictEqual(4)
    expect(form.getFieldValue('names')).toStrictEqual(undefined)
    expect(form.getFieldMeta('names')).toStrictEqual(undefined)
  })

  it("form's valid state should be work fine", () => {
    const form = new FormApi({
      defaultValues: {
        name: '',
      },
    })

    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChange: ({ value }) => (value.length > 0 ? undefined : 'required'),
      },
    })

    form.mount()

    field.mount()

    field.handleChange('one')

    expect(form.state.isFieldsValid).toEqual(true)
    expect(form.state.canSubmit).toEqual(true)

    field.handleChange('')

    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)

    field.handleChange('two')

    expect(form.state.isFieldsValid).toEqual(true)
    expect(form.state.canSubmit).toEqual(true)
  })

  it('should run validation onChange', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      validators: {
        onChange: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })

    const field = new FieldApi({
      form,
      name: 'name',
    })
    form.mount()
    field.mount()

    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should run async validation onChange', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      validators: {
        onChangeAsync: async ({ value }) => {
          await sleep(1000)
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })
    form.mount()

    field.mount()

    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    await vi.runAllTimersAsync()
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should run async validation onChange with debounce', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      validators: {
        onChangeAsyncDebounceMs: 1000,
        onChangeAsync: async ({ value }) => {
          await sleepMock(1000)
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })
    form.mount()

    field.mount()

    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    field.setValue('other', {
      dontUpdateMeta: true,
    })
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without onChangeAsyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should run async validation onChange with asyncDebounceMs', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      asyncDebounceMs: 1000,
      validators: {
        onChangeAsync: async ({ value }) => {
          await sleepMock(1000)
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    field.setValue('other', {
      dontUpdateMeta: true,
    })
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without asyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onChange: 'Please enter a different value',
    })
  })

  it('should run validation onBlur', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
      validators: {
        onBlur: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    field.setValue('other')
    field.validate('blur')
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onBlur', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      validators: {
        onBlurAsync: async ({ value }) => {
          await sleep(1000)
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()
    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    field.validate('blur')
    await vi.runAllTimersAsync()
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onBlur with debounce', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      validators: {
        onBlurAsyncDebounceMs: 1000,
        onBlurAsync: async ({ value }) => {
          await sleepMock(10)
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    field.validate('blur')
    field.validate('blur')
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without onBlurAsyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should run async validation onBlur with asyncDebounceMs', async () => {
    vi.useFakeTimers()
    const sleepMock = vi.fn().mockImplementation(sleep)

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      asyncDebounceMs: 1000,
      validators: {
        onBlurAsync: async ({ value }) => {
          await sleepMock(10)
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    expect(form.state.errors.length).toBe(0)
    field.setValue('other')
    field.validate('blur')
    field.validate('blur')
    await vi.runAllTimersAsync()
    // sleepMock will have been called 2 times without asyncDebounceMs
    expect(sleepMock).toHaveBeenCalledTimes(1)
    expect(form.state.errors).toContain('Please enter a different value')
    expect(form.state.errorMap).toMatchObject({
      onBlur: 'Please enter a different value',
    })
  })

  it('should contain multiple errors when running validation onBlur and onChange', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
      validators: {
        onBlur: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
        onChange: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    field.setValue('other')
    field.validate('blur')
    expect(form.state.errors).toStrictEqual([
      'Please enter a different value',
      'Please enter a different value',
    ])
    expect(form.state.errorMap).toEqual({
      onBlur: 'Please enter a different value',
      onChange: 'Please enter a different value',
    })
  })

  it('should return all errors', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
        age: 'hi',
      },
      validators: {
        onChange: ({ value }) => {
          if (value.name === 'other') return 'onChange - form'
          return
        },
        onMount: ({ value }) => {
          if (value.name === 'other') return 'onMount - form'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
      validators: {
        onChange: ({ value }) => {
          if (value === 'other') {
            return 'onChange - field'
          }
          return
        },
      },
    })

    form.mount()
    field.mount()
    expect(form.getAllErrors()).toEqual({
      fields: {},
      form: {
        errors: ['onMount - form'],
        errorMap: { onMount: 'onMount - form' },
      },
    })

    field.setValue('other')
    expect(form.getAllErrors()).toEqual({
      fields: {
        name: {
          errors: ['onChange - field'],
          errorMap: { onChange: 'onChange - field' },
        },
      },
      form: {
        errors: ['onChange - form'],
        errorMap: { onChange: 'onChange - form' },
      },
    })
  })

  it('should reset onChange errors when the issue is resolved', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
      validators: {
        onChange: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    field.setValue('other')
    expect(form.state.errors).toStrictEqual(['Please enter a different value'])
    expect(form.state.errorMap).toEqual({
      onChange: 'Please enter a different value',
    })
    field.setValue('test')
    expect(form.state.errors).toStrictEqual([])
    expect(form.state.errorMap).toEqual({})
  })

  it('should return error onMount', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
      validators: {
        onMount: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    expect(form.state.errors).toStrictEqual(['Please enter a different value'])
    expect(form.state.errorMap).toEqual({
      onMount: 'Please enter a different value',
    })
  })

  it('should remove onMount error when the form is touched', () => {
    const form = new FormApi({
      defaultValues: {
        name: 'other',
      },
      validators: {
        onMount: ({ value }) => {
          if (value.name === 'other') return 'Please enter a different value'
          return
        },
      },
    })
    const field = new FieldApi({
      form,
      name: 'name',
    })

    form.mount()
    field.mount()

    expect(form.state.errors).toStrictEqual(['Please enter a different value'])
    expect(form.state.errorMap).toEqual({
      onMount: 'Please enter a different value',
    })

    form.setFieldValue('name', 'test')
    expect(form.state.errors).toStrictEqual([])
  })

  it('should validate fields during submit', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
    })

    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
      validators: {
        onChange: ({ value }) =>
          value.length > 0 ? undefined : 'last name is required',
      },
    })

    field.mount()
    lastNameField.mount()

    await form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(form.state.fieldMeta['firstName']!.errors).toEqual([
      'first name is required',
    ])
    expect(form.state.fieldMeta['lastName']!.errors).toEqual([
      'last name is required',
    ])
  })

  it('should validate optional object fields during submit', async () => {
    const form = new FormApi({
      defaultValues: {
        person: null,
      } as { person: { firstName: string; lastName: string } | null },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'person.firstName',
      validators: {
        onChange: ({ value }) =>
          value && value.length > 0 ? undefined : 'first name is required',
      },
    })

    const lastNameField = new FieldApi({
      form,
      name: 'person.lastName',
      validators: {
        onChange: ({ value }) =>
          value && value.length > 0 ? undefined : 'last name is required',
      },
    })

    field.mount()
    lastNameField.mount()

    await form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(form.state.fieldMeta['person.firstName']!.errors).toEqual([
      'first name is required',
    ])
    expect(form.state.fieldMeta['person.lastName']!.errors).toEqual([
      'last name is required',
    ])
  })

  it('should run all types of validation on fields during submit', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
        onBlur: ({ value }) =>
          value.length > 3
            ? undefined
            : 'first name must be longer than 3 characters',
      },
    })

    field.mount()

    await form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(form.state.fieldMeta['firstName']!.errors).toEqual([
      'first name is required',
      'first name must be longer than 3 characters',
    ])
  })

  it('should run form submit validation once during submit, not once per field', async () => {
    const formSubmit = vi.fn().mockReturnValue(false)

    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
        age: 0,
      },
      validators: {
        onSubmit: formSubmit,
      },
    })
    form.mount()

    new FieldApi({ form, name: 'firstName' }).mount()
    new FieldApi({ form, name: 'lastName' }).mount()
    new FieldApi({ form, name: 'age' }).mount()

    await form.handleSubmit()

    expect(formSubmit).toHaveBeenCalledOnce()
  })

  it('should run form async submit validation once during submit', async () => {
    vi.useFakeTimers()
    const formSubmit = vi.fn()
    const fieldChangeValidator = vi
      .fn()
      .mockImplementation(async ({ value }) => {
        await sleep(1000)
        return value.length > 0 ? undefined : 'first name is required'
      })

    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
      validators: {
        onSubmitAsync: formSubmit,
      },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChangeAsync: fieldChangeValidator,
      },
    })

    field.mount()
    field.handleChange('test')

    await vi.runAllTimersAsync()
    expect(fieldChangeValidator).toHaveBeenCalledOnce()

    form.handleSubmit()
    await vi.runAllTimersAsync()

    expect(form.state.isFieldsValid).toEqual(true)
    expect(form.state.isValid).toEqual(true)
    expect(form.state.canSubmit).toEqual(true)

    expect(fieldChangeValidator).toHaveBeenCalledTimes(2)
    expect(formSubmit).toHaveBeenCalledOnce()
  })

  it('should run all types of async validation on fields during submit', async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChangeAsync: async ({ value }) => {
          await sleep(1000)
          return value.length > 0 ? undefined : 'first name is required'
        },
        onBlurAsync: async ({ value }) => {
          await sleep(1000)
          return value.length > 3
            ? undefined
            : 'first name must be longer than 3 characters'
        },
      },
    })

    field.mount()

    form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(true)
    await vi.runAllTimersAsync()
    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(form.state.fieldMeta['firstName']!.errorMap).toEqual({
      onChange: 'first name is required',
      onBlur: 'first name must be longer than 3 characters',
    })
  })

  it('should clear onSubmit error when a valid value is entered', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onSubmit: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
    })

    field.mount()

    await form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(form.state.fieldMeta['firstName']!.errorMap['onSubmit']).toEqual(
      'first name is required',
    )
    field.handleChange('test')
    expect(form.state.isFieldsValid).toEqual(true)
    expect(form.state.canSubmit).toEqual(true)
    expect(
      form.state.fieldMeta['firstName']!.errorMap['onSubmit'],
    ).toBeUndefined()
  })

  it('should validate all fields consistently', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
    })

    form.mount()
    field.mount()

    await form.validateAllFields('change')
    expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
    await form.validateAllFields('change')
    expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
  })

  it('should validate a single field consistently if touched', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })

    const field = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: ({ value }) =>
          value.length > 0 ? undefined : 'first name is required',
      },
      defaultMeta: {
        isTouched: true,
      },
    })

    form.mount()
    field.mount()

    await form.validateField('firstName', 'change')
    expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
    await form.validateField('firstName', 'change')
    expect(field.getMeta().errorMap.onChange).toEqual('first name is required')
  })

  it('should show onSubmit errors', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onSubmit: ({ value }) =>
          value.firstName.length > 0 ? undefined : 'first name is required',
      },
    })

    form.mount()

    const field = new FieldApi({
      form,
      name: 'firstName',
    })

    field.mount()

    await form.handleSubmit()
    expect(form.state.errors).toStrictEqual(['first name is required'])
  })

  it('should run onChange validation during submit', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onChange: ({ value }) =>
          value.firstName.length > 0 ? undefined : 'first name is required',
      },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'firstName',
    })

    field.mount()

    await form.handleSubmit()
    expect(form.state.errors).toStrictEqual(['first name is required'])
  })

  it('should run the form listener onSubmit', async () => {
    let triggered!: string
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      listeners: {
        onSubmit: ({ formApi }) => {
          triggered = formApi.state.values.name
        },
      },
    })

    form.mount()
    await form.handleSubmit()

    expect(triggered).toStrictEqual('test')
  })

  it('should run the form listener onMount', async () => {
    let triggered!: string
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      listeners: {
        onMount: ({ formApi }) => {
          triggered = formApi.state.values.name
        },
      },
    })

    form.mount()

    expect(triggered).toStrictEqual('test')
  })

  it('should run the form listener onChange', async () => {
    let fieldApiCheck!: AnyFieldApi
    let formApiCheck!: AnyFormApi

    const form = new FormApi({
      defaultValues: {
        name: '',
      },
      listeners: {
        onChange: ({ fieldApi, formApi }) => {
          fieldApiCheck = fieldApi

          formApiCheck = formApi as any
        },
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })
    field.mount()
    field.setValue('newTest')

    expect(fieldApiCheck.state.value).toStrictEqual('newTest')
    expect(formApiCheck.state.values.name).toStrictEqual('newTest')
  })

  it('should run the form listener onChange when the field array is changed', () => {
    let arr!: any

    const form = new FormApi({
      defaultValues: {
        items: ['one', 'two'],
        age: 0,
      },
      listeners: {
        onChange: ({ fieldApi }) => {
          arr = fieldApi.state.value
        },
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'items',
    })
    field.mount()

    field.removeValue(1)
    expect(arr).toStrictEqual(['one'])

    field.replaceValue(0, 'start')
    expect(arr).toStrictEqual(['start'])

    field.pushValue('end')
    expect(arr).toStrictEqual(['start', 'end'])

    field.insertValue(1, 'middle')
    expect(arr).toStrictEqual(['start', 'middle', 'end'])

    field.swapValues(0, 2)
    expect(arr).toStrictEqual(['end', 'middle', 'start'])

    field.moveValue(0, 1)
    expect(arr).toStrictEqual(['middle', 'end', 'start'])
  })

  it('should debounce onChange listener', async () => {
    vi.useFakeTimers()
    const onChangeMock = vi.fn()

    const form = new FormApi({
      defaultValues: {
        name: '',
      },
      listeners: {
        onChange: onChangeMock,
        onChangeDebounceMs: 500,
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })
    field.mount()

    field.handleChange('first')
    field.handleChange('second')
    expect(onChangeMock).not.toHaveBeenCalled()

    await vi.advanceTimersByTimeAsync(500)
    expect(onChangeMock).toHaveBeenCalledTimes(1)
    expect(onChangeMock).toHaveBeenCalledWith({
      formApi: form,
      fieldApi: field,
    })
  })

  it('should run the form listener onBlur', async () => {
    let fieldApiCheck!: AnyFieldApi
    let formApiCheck!: AnyFormApi

    const form = new FormApi({
      defaultValues: {
        name: 'test',
        age: 0,
      },
      listeners: {
        onBlur: ({ fieldApi, formApi }) => {
          fieldApiCheck = fieldApi

          formApiCheck = formApi as any
        },
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })
    field.mount()
    field.handleBlur()

    expect(fieldApiCheck.state.value).toStrictEqual('test')
    expect(formApiCheck.state.values.name).toStrictEqual('test')
  })

  it('should debounce onBlur listener', async () => {
    vi.useFakeTimers()
    const onBlurMock = vi.fn()

    const form = new FormApi({
      defaultValues: {
        name: '',
      },
      listeners: {
        onBlur: onBlurMock,
        onBlurDebounceMs: 500,
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })
    field.mount()

    field.handleBlur()
    field.handleBlur()
    expect(onBlurMock).not.toHaveBeenCalled()

    await vi.advanceTimersByTimeAsync(500)
    expect(onBlurMock).toHaveBeenCalledTimes(1)
  })

  it('should run both onBlur and onChange listeners when onBlurDebounceMs and onChangeDebounceMs are provided', async () => {
    vi.useFakeTimers()
    const onBlurMock = vi.fn()
    const onChangeMock = vi.fn()

    const form = new FormApi({
      defaultValues: {
        name: 'test',
        age: 0,
      },
      listeners: {
        onBlur: onBlurMock,
        onBlurDebounceMs: 500,
        onChange: onChangeMock,
        onChangeDebounceMs: 500,
      },
    })
    form.mount()

    const field = new FieldApi({
      form,
      name: 'name',
    })
    field.mount()
    field.handleBlur()
    field.handleChange('test')

    await vi.advanceTimersByTimeAsync(500)
    expect(onBlurMock).toHaveBeenCalledTimes(1)
    expect(onChangeMock).toHaveBeenCalledTimes(1)

    field.handleChange('test2')
    field.handleBlur()

    await vi.advanceTimersByTimeAsync(500)
    expect(onBlurMock).toHaveBeenCalledTimes(2)
    expect(onChangeMock).toHaveBeenCalledTimes(2)
  })

  it('should run the field listener onSubmit', async () => {
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    let triggered!: string
    const field = new FieldApi({
      form,
      name: 'name',
      listeners: {
        onSubmit: ({ value }) => {
          triggered = value
        },
      },
    })

    field.mount()
    await form.handleSubmit()

    expect(triggered).toStrictEqual('test')
  })

  it('should update a nullable object', async () => {
    const form = new FormApi({
      defaultValues: {
        person: null,
      } as { person: { firstName: string } | null },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'person.firstName',
    })

    field.mount()

    field.setValue('firstName', {
      dontUpdateMeta: true,
    })
    expect(form.state.values.person?.firstName).toStrictEqual('firstName')
  })

  it('should update a deep nullable object', async () => {
    const form = new FormApi({
      defaultValues: {
        person: null,
      } as { person: { nameInfo: { first: string } | null } | null },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'person.nameInfo.first',
    })

    field.mount()

    field.setValue('firstName', {
      dontUpdateMeta: true,
    })
    expect(form.state.values.person?.nameInfo?.first).toStrictEqual('firstName')
  })

  it('should update a nullable array', async () => {
    const form = new FormApi({
      defaultValues: {
        persons: null,
      } as { persons: Array<{ nameInfo: { first: string } }> | null },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'persons',
    })

    field.mount()

    field.pushValue({ nameInfo: { first: 'firstName' } })
    expect(form.state.values.persons).toStrictEqual([
      { nameInfo: { first: 'firstName' } },
    ])
  })

  it('should add a new value to the formApi errorMap', () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })
    form.mount()
    form.setErrorMap({
      onChange: "name can't be Josh",
    } as never)
    expect(form.state.errorMap.onChange).toEqual("name can't be Josh")
  })

  it('should preserve other values in the formApi errorMap when adding other values', () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })
    form.mount()
    form.setErrorMap({
      onChange: "name can't be Josh",
    } as never)
    expect(form.state.errorMap.onChange).toEqual("name can't be Josh")
    form.setErrorMap({
      onBlur: 'name must begin with uppercase',
    } as never)
    expect(form.state.errorMap.onChange).toEqual("name can't be Josh")
    expect(form.state.errorMap.onBlur).toEqual('name must begin with uppercase')
  })

  it('should replace errorMap value if it exists in the FormApi object', () => {
    interface Form {
      name: string
    }
    const form = new FormApi({ defaultValues: {} as Form })
    form.mount()
    form.setErrorMap({
      onChange: "name can't be Josh",
    } as never)
    expect(form.state.errorMap.onChange).toEqual("name can't be Josh")
    form.setErrorMap({
      onChange: 'other validation error',
    } as never)
    expect(form.state.errorMap.onChange).toEqual('other validation error')
  })

  it('should spread errors in fields when setErrorMap receives a global form validation error', () => {
    const form = new FormApi({
      defaultValues: { name: '', interests: [] as { label: string }[] },
    })
    form.mount()

    const field = new FieldApi({ form, name: 'name' })
    field.mount()

    const arrayElementField = new FieldApi({ form, name: 'interests[0].label' })
    arrayElementField.mount()

    form.setErrorMap({
      onChange: {
        form: 'global error',
        fields: {
          name: 'name is required',
          'interests[0].label': 'label is required',
        },
      },
      onBlur: 'Form Error' as never,
    })

    expect(form.state.errorMap.onChange).toEqual('global error')
    expect(form.state.errorMap.onBlur).toEqual('Form Error')
    expect(field.getMeta().errorMap.onChange).toEqual('name is required')
    expect(arrayElementField.getMeta().errorMap.onChange).toEqual(
      'label is required',
    )
  })

  it("should set errors for the fields from the form's onSubmit validator", async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onSubmit: ({ value }) => {
          if (value.firstName.length === 0) {
            return {
              form: 'something went wrong',
              fields: {
                firstName: 'first name is required',
              },
            }
          }

          return null
        },
      },
    })

    form.mount()

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onSubmit: ({ value }) => {
          if (value === 'nothing') return 'value cannot be "nothing"'
          return null
        },
      },
    })

    firstNameField.mount()

    // Check if the error is returned from the form's onSubmit validator
    await form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(firstNameField.state.meta.errorMap.onSubmit).toBe(
      'first name is required',
    )
    expect(form.state.errorMap.onSubmit).toBe('something went wrong')

    // Check if the error is gone after the value is changed
    firstNameField.setValue('nothing')
    // Handling the blur is needed, because the `blur` error on the field
    // is not cleared up before `handleSubmit` is called, so the field
    // is considered to be invalid.
    firstNameField.handleBlur()
    await form.handleSubmit()

    expect(firstNameField.state.meta.errorMap.onSubmit).toBe(
      'value cannot be "nothing"',
    )

    // Check if the error from the field's validator is shown
    firstNameField.setValue('something else')
    await form.handleSubmit()
    expect(firstNameField.state.meta.errorMap.onSubmit).toBe(undefined)
    expect(form.state.errors).toStrictEqual([])
  })

  it('should run validators in order form sync -> field sync -> form async -> field async', async () => {
    const order: string[] = []
    const formAsyncChange = vi.fn().mockImplementation(async () => {
      order.push('formAsyncChange')
      await sleep(1000)
    })
    const formSyncChange = vi.fn().mockImplementation(() => {
      order.push('formSyncChange')
    })
    const fieldAsyncChange = vi.fn().mockImplementation(async () => {
      order.push('fieldAsyncChange')
      await sleep(1000)
    })
    const fieldSyncChange = vi.fn().mockImplementation(() => {
      order.push('fieldSyncChange')
    })

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onChange: formSyncChange,
        onChangeAsync: formAsyncChange,
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: fieldSyncChange,
        onChangeAsync: fieldAsyncChange,
      },
    })

    form.mount()
    firstNameField.mount()

    firstNameField.handleChange('something')
    await vi.runAllTimersAsync()

    expect(order).toStrictEqual([
      'formSyncChange',
      'fieldSyncChange',
      'formAsyncChange',
      'fieldAsyncChange',
    ])
  })

  it('should not run form async validator if field sync has errored', async () => {
    const formAsyncChange = vi.fn()
    const formSyncChange = vi.fn()

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onChange: formSyncChange,
        onChangeAsync: formAsyncChange,
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: ({ value }) => (value.length > 0 ? undefined : 'field error'),
      },
    })

    form.mount()
    firstNameField.mount()

    firstNameField.handleChange('')
    await vi.runAllTimersAsync()

    expect(formSyncChange).toHaveBeenCalled()
    expect(firstNameField.state.meta.errorMap.onChange).toBe('field error')
    expect(formAsyncChange).not.toHaveBeenCalled()
  })

  it('runs form async validator if field sync has errored and asyncAlways is true', async () => {
    const formAsyncChange = vi.fn()
    const formSyncChange = vi.fn()

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onChange: formSyncChange,
        onChangeAsync: formAsyncChange,
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
      asyncAlways: true,
      validators: {
        onChange: ({ value }) => (value.length > 0 ? undefined : 'field error'),
      },
    })

    form.mount()
    firstNameField.mount()

    firstNameField.handleChange('')
    await vi.runAllTimersAsync()

    expect(formSyncChange).toHaveBeenCalled()
    expect(firstNameField.state.meta.errorMap.onChange).toBe('field error')
    expect(formAsyncChange).toHaveBeenCalled()
  })

  it("should set errors for the fields from the form's onChange validator", async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: 'something',
      },
      validators: {
        onChange: ({ value }) => {
          if (value.firstName.length === 0) {
            return {
              fields: {
                firstName: 'first name is required',
              },
            }
          }

          return null
        },
      },
    })
    form.mount()
    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
      validators: {
        onChange: ({ value }) => {
          if (value === 'nothing') return 'value cannot be "nothing"'

          return null
        },
      },
    })

    firstNameField.mount()

    // Check if we get an error from the form's `onChange` validator
    firstNameField.setValue('')

    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(firstNameField.state.meta.errorMap.onChange).toBe(
      'first name is required',
    )

    // Check if we can make the error go away by changing the value
    firstNameField.setValue('one')
    expect(firstNameField.state.meta.errorMap.onChange).toBe(undefined)

    // Check if we get an error from the field's `onChange` validator
    firstNameField.setValue('nothing')

    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(firstNameField.state.meta.errorMap.onChange).toBe(
      'value cannot be "nothing"',
    )

    // Check if we can make the error go away by changing the value
    firstNameField.setValue('one')
    expect(firstNameField.state.meta.errorMap.onChange).toBe(undefined)
  })

  it("should remove the onSubmit errors set from the form's validators after the field has been touched", async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onSubmit: ({ value }) => {
          if (value.firstName.length === 0) {
            return {
              form: 'something went wrong',
              fields: {
                firstName: 'first name is required',
              },
            }
          }

          return null
        },
      },
    })
    form.mount()
    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })

    firstNameField.mount()

    await form.handleSubmit()

    expect(firstNameField.state.meta.errorMap.onSubmit).toEqual(
      'first name is required',
    )

    firstNameField.setValue('this is a first name')

    expect(firstNameField.state.meta.errorMap.onSubmit).toBe(undefined)
  })

  it("should set errors for the fields from the form's onSubmit validator for array fields", async () => {
    const form = new FormApi({
      defaultValues: {
        names: ['person'],
      },
      validators: {
        onSubmit: ({ value }) => {
          return value.names.includes('person-2')
            ? {
                fields: {
                  names: 'person-2 cannot be used',
                },
              }
            : undefined
        },
      },
    })

    form.mount()

    const namesField = new FieldApi({ form, name: 'names' })
    namesField.mount()

    namesField.setValue((value) => [...value, 'person-2'])

    await form.handleSubmit()

    expect(namesField.state.meta.errorMap.onSubmit).toBe(
      'person-2 cannot be used',
    )
  })

  it("should set errors for the fields from the form's onSubmitAsync validator for array fields", async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        names: ['test'],
      },
      asyncDebounceMs: 1,
      validators: {
        onSubmitAsync: async ({ value }) => {
          await sleep(1)
          if (value.names.includes('other')) {
            return { fields: { names: 'Please enter a different value' } }
          }
          return
        },
      },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'names',
    })

    field.mount()
    field.pushValue('other')

    expect(field.state.meta.errors.length).toBe(0)

    form.handleSubmit()
    await vi.runAllTimersAsync()

    expect(field.state.meta.errorMap.onSubmit).toBe(
      'Please enter a different value',
    )

    field.removeValue(1)
    form.handleSubmit()
    await vi.runAllTimersAsync()

    expect(field.state.value).toStrictEqual(['test'])
    expect(field.state.meta.errors.length).toBe(0)
    expect(field.state.meta.errorMap.onSubmit).toBe(undefined)
  })

  it("should be able to set errors on nested field inside of an array from the form's validators", async () => {
    interface Employee {
      firstName: string
    }
    interface Form {
      employees: Partial<Employee>[]
    }

    const form = new FormApi({
      validators: {
        onSubmit: ({ value }) => {
          const fieldWithErrorIndex = value.employees.findIndex(
            (val) => val.firstName === 'person-2',
          )

          if (fieldWithErrorIndex !== -1) {
            return {
              fields: {
                [`employees[${fieldWithErrorIndex}].firstName`]:
                  'person-2 is banned from registering',
              },
            }
          }
          return null
        },
      },
      defaultValues: {} as Form,
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'employees',
      defaultValue: [],
    })

    field.mount()

    const fieldInArray = new FieldApi({
      form,
      name: `employees[0].firstName`,
      defaultValue: 'person-2',
    })

    fieldInArray.mount()
    await form.handleSubmit()

    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(fieldInArray.state.meta.errorMap.onSubmit).toBe(
      'person-2 is banned from registering',
    )

    fieldInArray.setValue('Somebody else')

    await form.handleSubmit()
    expect(form.state.isFieldsValid).toEqual(true)
    expect(form.state.canSubmit).toEqual(true)
    expect(fieldInArray.state.meta.errors.length).toBe(0)

    await form.handleSubmit()
  })

  it("should set errors on a linked field from the form's onChange validator", async () => {
    const form = new FormApi({
      defaultValues: {
        password: '',
        confirm_password: '',
      },
      validators: {
        onChange: ({ value }) => {
          if (value.confirm_password !== value.password) {
            return {
              fields: {
                confirm_password: 'passwords do not match',
              },
            }
          }
          return null
        },
      },
    })
    form.mount()
    const passField = new FieldApi({
      form,
      name: 'password',
    })

    const passconfirmField = new FieldApi({
      form,
      name: 'confirm_password',
      validators: {
        onChangeListenTo: ['password'],
      },
    })

    passField.mount()
    passconfirmField.mount()

    passField.setValue('one')

    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(passconfirmField.state.meta.errorMap.onChange).toBe(
      'passwords do not match',
    )

    passconfirmField.setValue('one')
    expect(form.state.isFieldsValid).toEqual(true)
    expect(form.state.canSubmit).toEqual(true)
    expect(passconfirmField.state.meta.errors.length).toBe(0)
  })

  it("should set errors on a linked field from the form's onChangeAsync validator", async () => {
    vi.useFakeTimers()

    const form = new FormApi({
      defaultValues: {
        password: '',
        confirm_password: '',
      },
      validators: {
        onChangeAsync: async ({ value }) => {
          if (value.confirm_password !== value.password) {
            return {
              fields: {
                confirm_password: 'passwords do not match',
              },
            }
          }
          return null
        },
      },
    })
    form.mount()
    const passField = new FieldApi({
      form,
      name: 'password',
    })

    const passconfirmField = new FieldApi({
      form,
      name: 'confirm_password',
      validators: {
        onChangeListenTo: ['password'],
      },
    })

    passField.mount()
    passconfirmField.mount()

    passField.setValue('one')

    await vi.runAllTimersAsync()

    expect(form.state.isFieldsValid).toEqual(false)
    expect(form.state.canSubmit).toEqual(false)
    expect(passconfirmField.state.meta.errorMap.onChange).toBe(
      'passwords do not match',
    )

    passconfirmField.setValue('one')

    await vi.runAllTimersAsync()

    expect(form.state.isFieldsValid).toBe(true)
    expect(form.state.canSubmit).toBe(true)
    expect(passconfirmField.state.meta.errors.length).toBe(0)
  })

  it("should set field errors from the form's onMount validator", async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      validators: {
        onMount: () => {
          return {
            form: 'something went wrong',
            fields: {
              firstName: 'first name is required',
            },
          }
        },
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })

    firstNameField.mount()
    form.mount()

    expect(form.state.errorMap.onMount).toBe('something went wrong')
    expect(firstNameField.state.meta.errorMap.onMount).toBe(
      'first name is required',
    )
  })

  it('clears errors on all fields affected by form validation when condition resolves', () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
      validators: {
        onChange: ({ value }) => {
          if (value.firstName && value.lastName) {
            return {
              fields: {
                firstName: 'Do not enter both firstName and lastName',
                lastName: 'Do not enter both firstName and lastName',
              },
            }
          }
          return null
        },
      },
    })
    form.mount()

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })
    firstNameField.mount()

    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })
    lastNameField.mount()

    // Set values to trigger validation errors
    firstNameField.setValue('John')
    lastNameField.setValue('Doe')

    // Verify both fields have errors
    expect(firstNameField.state.meta.errors).toContain(
      'Do not enter both firstName and lastName',
    )
    expect(lastNameField.state.meta.errors).toContain(
      'Do not enter both firstName and lastName',
    )

    // Clear one field's value
    firstNameField.setValue('')

    // Verify both fields have their errors cleared
    expect(firstNameField.state.meta.errors).toStrictEqual([])
    expect(lastNameField.state.meta.errors).toStrictEqual([])
  })

  it('clears previous form level errors for subfields when they are no longer valid', () => {
    const form = new FormApi({
      defaultValues: {
        interests: [
          { interestName: 'Interest 1' },
          { interestName: 'Interest 2' },
        ],
      },
      validators: {
        onChange: ({ value }) => {
          const interestNames = value.interests.map(
            (interest) => interest.interestName,
          )
          const uniqueInterestNames = new Set(interestNames)

          if (uniqueInterestNames.size !== interestNames.length) {
            return {
              fields: {
                interests: 'No duplicate interests allowed',
              },
            }
          }

          return null
        },
      },
    })
    form.mount()

    const interestsField = new FieldApi({
      form,
      name: 'interests',
    })
    interestsField.mount()

    const field0 = new FieldApi({
      form,
      name: 'interests[0].interestName',
    })
    field0.mount()

    const field1 = new FieldApi({
      form,
      name: 'interests[1].interestName',
    })
    field1.mount()

    // When creating a duplicate interest via form level validator
    field1.setValue('Interest 1')
    expect(interestsField.state.meta.errors).toStrictEqual([
      'No duplicate interests allowed',
    ])

    // When fixing the duplicate interest via form level validator
    field1.setValue('Interest 2')
    expect(interestsField.state.meta.errors).toStrictEqual([])
  })

  it('should not change the onBlur state of the fields when the form is submitted', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
        lastName: '',
      },
    })

    const firstNameField = new FieldApi({
      form,
      name: 'firstName',
    })
    firstNameField.mount()

    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })
    lastNameField.mount()

    firstNameField.handleBlur()

    expect(firstNameField.state.meta.isBlurred).toBe(true)

    await form.handleSubmit()

    expect(firstNameField.state.meta.isBlurred).toBe(true)
    expect(lastNameField.state.meta.isBlurred).toBe(false)
  })

  it('should pass the handleSubmit meta data to onSubmit', async () => {
    const form = new FormApi({
      onSubmitMeta: {} as { dinosaur: string },
      onSubmit: async ({ meta }) => {
        expect(meta.dinosaur).toEqual('Stegosaurus')
      },
    })

    await form.handleSubmit({ dinosaur: 'Stegosaurus' })
  })

  it('should pass the handleSubmit meta data to onSubmitInvalid', async () => {
    const form = new FormApi({
      onSubmitMeta: {} as { dinosaur: string },
      onSubmitInvalid: async ({ meta }) => {
        expect(meta.dinosaur).toEqual('Stegosaurus')
      },
    })

    await form.handleSubmit({ dinosaur: 'Stegosaurus' })
  })

  it('should pass the handleSubmit meta data to the onSubmit listener', async () => {
    const form = new FormApi({
      onSubmitMeta: {} as { dinosaur: string },
      listeners: {
        onSubmit: ({ meta }) => {
          expect(meta.dinosaur).toEqual('Stegosaurus')
        },
      },
    })

    await form.handleSubmit({ dinosaur: 'Stegosaurus' })
  })

  it('should pass the handleSubmit default meta data to onSubmit', async () => {
    const form = new FormApi({
      onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
      onSubmit: async ({ meta }) => {
        expect(meta.dinosaur).toEqual('Frank')
      },
    })

    await form.handleSubmit()
  })

  it('should call onSubmitInvalid when submitting while canSubmit is false (e.g., onMount error present)', async () => {
    const onInvalid = vi.fn()

    const form = new FormApi({
      defaultValues: { name: '' },
      validators: {
        onMount: ({ value }) => (!value.name ? 'Name required' : undefined),
      },
      onSubmitInvalid: ({ value, formApi }) => {
        onInvalid(value, formApi)
      },
    })

    form.mount()

    // Mount a field to participate in touched/dirty state
    new FieldApi({ form, name: 'name' }).mount()

    // With an onMount error present, the form is invalid and cannot submit
    expect(form.state.canSubmit).toBe(false)

    await form.handleSubmit()

    expect(onInvalid).toHaveBeenCalledTimes(1)
  })

  it('should pass the handleSubmit default meta data to onSubmitInvalid', async () => {
    const form = new FormApi({
      onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
      onSubmitInvalid: async ({ meta }) => {
        expect(meta.dinosaur).toEqual('Frank')
      },
    })

    await form.handleSubmit()
  })

  it('should pass the handleSubmit default meta data to the onSubmit listener', async () => {
    const form = new FormApi({
      onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
      listeners: {
        onSubmit: ({ meta }) => {
          expect(meta.dinosaur).toEqual('Frank')
        },
      },
    })

    await form.handleSubmit()
  })

  it('should read and update union objects', async () => {
    const form = new FormApi({
      defaultValues: {
        person: { firstName: 'firstName' },
      } as { person?: { firstName: string } | { age: number } | null },
    })

    const field = new FieldApi({
      form,
      name: 'person.firstName',
    })
    field.mount()
    expect(field.getValue()).toStrictEqual('firstName')

    form.setFieldValue('person', { age: 0 })

    const field2 = new FieldApi({
      form,
      name: 'person.age',
    })
    field2.mount()
    expect(field2.getValue()).toStrictEqual(0)
  })

  it('should update isSubmitSuccessful correctly during form submission', async () => {
    const onSubmit = vi.fn().mockResolvedValue(undefined)
    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
      onSubmit,
    })

    form.mount()

    expect(form.state.isSubmitSuccessful).toBe(false)

    await form.handleSubmit()

    expect(form.state.isSubmitSuccessful).toBe(true)
    expect(onSubmit).toHaveBeenCalledTimes(1)

    // Simulate a failed submission
    onSubmit.mockRejectedValueOnce(new Error('Submission failed'))

    await expect(form.handleSubmit()).rejects.toThrow('Submission failed')

    expect(form.state.isSubmitSuccessful).toBe(false)
  })

  it('should reset the fields value and meta to default state', async () => {
    const form = new FormApi({
      defaultValues: {
        name: 'tony',
      } as { name: string },
    })
    form.mount()
    const field = new FieldApi({
      form,
      name: 'name',
    })

    field.mount()
    field.setValue('hawk')

    expect(form.state.values.name).toStrictEqual('hawk')
    expect(field.state.meta.isTouched).toBe(true)

    form.resetField('name')
    expect(form.state.values.name).toStrictEqual('tony')
    expect(field.state.meta.isTouched).toBe(false)
  })

  it('should set the form isDefaultValue meta', async () => {
    const form = new FormApi({
      defaultValues: {
        name: 'tony',
        lastName: 'hawk',
      },
    })
    form.mount()

    const nameField = new FieldApi({
      form,
      name: 'name',
    })
    nameField.mount()

    const lastNameField = new FieldApi({
      form,
      name: 'lastName',
    })
    lastNameField.mount()

    lastNameField.setValue('')
    expect(form.state.isDefaultValue).toBe(false)

    lastNameField.setValue('hawk')
    expect(form.state.isDefaultValue).toBe(true)
  })

  it('should allow submission, when the form is invalid, with canSubmitWhenInvalid', async () => {
    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
      canSubmitWhenInvalid: true,
      validators: {
        onMount: () => {
          return {
            form: 'something went wrong',
            fields: {
              firstName: 'first name is required',
            },
          }
        },
      },
    })
    form.mount()
    expect(form.state.isValid).toBe(false)
    expect(form.state.canSubmit).toBe(true)
  })

  it('should pass the current values to the Standard Schema when calling parseValuesWithSchema', async () => {
    const schema = z.object({
      firstName: z.string().min(3),
    })

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })
    form.mount()

    // First name schema should complain that 'firstName' is too short
    const issueResult = form.parseValuesWithSchema(schema)
    expect(issueResult).toBeDefined()
    expect(Array.isArray(issueResult)).toBe(false)

    expect(issueResult).toHaveProperty('fields')
    expect(issueResult).toHaveProperty('form')

    expect(issueResult?.fields).toHaveProperty('firstName')
    expect(Array.isArray(issueResult?.fields['firstName'])).toBe(true)
    expect(issueResult?.fields['firstName']?.length).toBeGreaterThan(0)

    form.setFieldValue(
      'firstName',
      'some long name that satisfies firstNameSchemaResult',
    )
    // firstName should now be satisfied
    const successResult = form.parseValuesWithSchema(schema)
    expect(successResult).toBeUndefined()
  })

  it('should pass the current values to the Standard Schema when calling parseValuesWithSchemaAsync', async () => {
    vi.useFakeTimers()

    const schema = z.object({
      firstName: z.string().min(3),
    })

    const form = new FormApi({
      defaultValues: {
        firstName: '',
      },
    })
    form.mount()

    // First name schema should complain that 'firstName' is too short
    const issuePromise = form.parseValuesWithSchemaAsync(schema)
    expect(issuePromise).toBeInstanceOf(Promise)

    const issueResult = await issuePromise

    expect(issueResult).toBeDefined()
    expect(Array.isArray(issueResult)).toBe(false)

    expect(issueResult).toHaveProperty('fields')
    expect(issueResult).toHaveProperty('form')

    expect(issueResult?.fields).toHaveProperty('firstName')
    expect(Array.isArray(issueResult?.fields['firstName'])).toBe(true)
    expect(issueResult?.fields['firstName']?.length).toBeGreaterThan(0)

    form.setFieldValue(
      'firstName',
      'some long name that satisfies firstNameSchemaResult',
    )

    // firstName should now be satisfied
    const successPromise = form.parseValuesWithSchemaAsync(schema)
    expect(successPromise).toBeInstanceOf(Promise)

    const successResult = await successPromise
    expect(successResult).toBeUndefined()
  })

  it('should throw an error when passing an async Standard Schema to parseValuesWithSchema', async () => {
    const testSchema = z
      .object({
        name: z.string(),
      })
      .superRefine(async () => {
        await sleep(1000)
        return true
      })

    const form = new FormApi({
      defaultValues: {
        name: '',
      },
    })
    form.mount()

    // async passed to sync should error
    expect(() => {
      form.parseValuesWithSchema(testSchema)
    }).toThrowError()
    // async to async is fine
    expect(() => {
      form.parseValuesWithSchemaAsync(testSchema)
    }).not.toThrowError()
    // sync to async is also fine
    expect(() => {
      form.parseValuesWithSchemaAsync(z.any())
    }).not.toThrowError()
  })

  it('should delete fields when resetting an array field to an empty array', () => {
    const employees = [
      {
        firstName: 'Darcy',
      },
    ]

    const form = new FormApi({
      defaultValues: {
        employees,
      },
    })
    form.mount()

    form.clearFieldValues('employees')

    expect(form.getFieldValue('employees')).toEqual([])
    expect(form.getFieldValue(`employees[0]`)).toBeUndefined()
    expect(form.getFieldMeta(`employees[0]`)).toBeUndefined()
    expect(form.state.values.employees).toStrictEqual([])
  })
})

it('should reset the errorSourceMap for the field when the form is reset', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
    validators: {
      onChange: () => {
        return {
          fields: { name: 'Error' },
        }
      },
    },
  })
  form.mount()

  const field = new FieldApi({
    form,
    name: 'name',
  })
  field.mount()
  field.setValue('hawk')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('form')
  form.reset()
  expect(form.getFieldMeta('name')?.errorSourceMap).toEqual({})
})

it('should reset the errorSourceMap for the field when the field is reset', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
  })
  form.mount()

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: () => 'Error',
    },
  })
  field.mount()
  field.setValue('hawk')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('field')
  form.resetField('name')
  expect(form.getFieldMeta('name')?.errorSourceMap).toEqual({})
})

it('should set the errorSourceMap undefined when form level validator is resolved', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: {
            name: value.name !== 'tony' ? 'Error' : null,
          },
        }
      },
    },
  })
  form.mount()

  const field = new FieldApi({
    form,
    name: 'name',
  })
  field.mount()

  field.setValue('not tony')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('form')

  field.setValue('tony')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toBeUndefined()
})

it('should set the errorSourceMap undefined when field level validator is resolved', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
  })

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: ({ value }) => (value !== 'tony' ? 'Error' : undefined),
    },
  })
  field.mount()

  field.setValue('not tony')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('field')

  field.setValue('tony')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toBeUndefined()
})

it('should set errorSourceMap to form when field error is resolved but form error is not', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: {
            name: value.name === 'Tony' ? 'Name cannot be Tony' : null,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: ({ value }) =>
        value === 'John' ? 'Name cannot be John' : null,
    },
  })
  field.mount()

  field.setValue('John')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('field')

  field.setValue('Tony')

  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('form')
})

it('should prioritze field error over form error on errorSourceMap', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: {
            name: value.name === 'John' ? 'Error from form' : null,
          },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChange: ({ value }) => (value === 'John' ? 'Error from field' : null),
    },
  })

  field.mount()

  expect(field.getMeta().errorMap.onChange).toBeUndefined()
  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toBeUndefined()

  field.setValue('John')

  expect(field.getMeta().errorMap.onChange).toEqual('Error from field')
  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toEqual('field')

  field.setValue('Tony')

  expect(field.getMeta().errorMap.onChange).toBeUndefined()
  expect(form.getFieldMeta('name')?.errorSourceMap.onChange).toBeUndefined()
})

it('should run form level async validation onChange and update the errorSourceMap', async () => {
  vi.useFakeTimers()

  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
    validators: {
      onChangeAsync: async ({ value }) => {
        await sleep(1000)
        if (value.name === 'other')
          return {
            fields: {
              name: 'Please enter a different value',
            },
          }
        return null
      },
    },
  })
  const field = new FieldApi({
    form,
    name: 'name',
  })
  form.mount()

  field.mount()

  expect(form.state.errors.length).toBe(0)
  field.setValue('other')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorMap).toMatchObject({
    onChange: 'Please enter a different value',
  })

  expect(field.getMeta().errorSourceMap.onChange).toEqual('form')
})

it('should run field level async validation onChange and update the errorSourceMap', async () => {
  vi.useFakeTimers()

  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  })
  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChangeAsync: async ({ value }) => {
        await sleep(1000)
        if (value === 'other') return 'Please enter a different value'
        return null
      },
    },
  })
  form.mount()
  field.mount()

  field.setValue('other')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorMap).toMatchObject({
    onChange: 'Please enter a different value',
  })

  expect(field.getMeta().errorSourceMap.onChange).toEqual('field')
})

it('should shift sourceMap to form when async field error is resolved but async form error is not', async () => {
  vi.useFakeTimers()

  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: { name: value.name === 'John' ? 'Error from form' : null },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChangeAsync: async ({ value }) => {
        await sleep(1000)
        if (value === 'other') return 'Please enter a different value'
        return null
      },
    },
  })

  form.mount()
  field.mount()

  field.setValue('other')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorSourceMap.onChange).toEqual('field')

  field.setValue('John')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorSourceMap.onChange).toEqual('form')
})

it('should shift sourceMap to field when async form error is resolved but async field error is not', async () => {
  vi.useFakeTimers()

  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: { name: value.name === 'John' ? 'Error from form' : null },
        }
      },
    },
  })

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChangeAsync: async ({ value }) => {
        await sleep(1000)
        if (value === 'other') return 'Please enter a different value'
        return null
      },
    },
  })

  form.mount()
  field.mount()

  field.setValue('other')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorSourceMap.onChange).toEqual('field')

  field.setValue('John')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorSourceMap.onChange).toEqual('form')
})

it('should mark sourceMap as undefined when async field error is resolved', async () => {
  vi.useFakeTimers()
  const form = new FormApi({
    defaultValues: {
      name: 'tony',
    } as { name: string },
  })

  const field = new FieldApi({
    form,
    name: 'name',
    validators: {
      onChangeAsync: async ({ value }) => {
        await sleep(1000)
        if (value === 'other') return 'Please enter a different value'
        return null
      },
    },
  })

  form.mount()
  field.mount()

  field.setValue('other')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorSourceMap.onChange).toEqual('field')

  field.setValue('John')
  await vi.runAllTimersAsync()

  expect(field.getMeta().errorSourceMap.onChange).toBeUndefined()
})

it('should reset nested object fields', () => {
  const defaultValues = {
    shallow: '',
    nested: {
      field: {
        name: '',
      },
    },
  }

  const form = new FormApi({
    defaultValues,
  })
  form.mount()

  form.setFieldValue('shallow', 'Shallow')
  form.setFieldValue('nested.field.name', 'Nested')

  expect(form.state.values.shallow).toEqual('Shallow')
  expect(form.state.values.nested.field.name).toEqual('Nested')

  form.resetField('shallow')
  expect(form.state.values.shallow).toEqual('')

  form.resetField('nested.field.name')
  expect(form.state.values.nested.field.name).toEqual('')
})

it('should reset nested array fields', () => {
  const defaultValues = {
    shallow: '',
    nested: {
      arr: [{ name: '' }, { test: 'array-test' }],
    },
  }

  const form = new FormApi({
    defaultValues,
  })
  form.mount()

  form.setFieldValue('shallow', 'Shallow')
  form.setFieldValue('nested.arr[0].name', 'nested-arr')
  form.setFieldValue('nested.arr[1].test', 'array-test-changed')

  expect(form.state.values.shallow).toEqual('Shallow')
  expect(form.state.values.nested.arr[0]?.name).toEqual('nested-arr')
  expect(form.state.values.nested.arr[1]?.test).toEqual('array-test-changed')

  form.resetField('shallow')
  expect(form.state.values.shallow).toEqual('')

  form.resetField('nested.arr[0].name')
  expect(form.state.values.nested.arr[0]?.name).toEqual('')
  expect(form.state.values.nested.arr[1]?.test).toEqual('array-test-changed')
})

it('should preserve nested fields on resetField if defaultValues is not provided', () => {
  const state = {
    shallow: '',
    nested: {
      field: {
        name: '',
      },
    },
  }

  const form = new FormApi({
    defaultState: { values: state },
  })
  form.mount()

  form.setFieldValue('shallow', 'Shallow')
  form.setFieldValue('nested.field.name', 'Nested')

  expect(form.state.values.shallow).toEqual('Shallow')
  expect(form.state.values.nested.field.name).toEqual('Nested')

  form.resetField('shallow')
  expect(form.state.values.shallow).toEqual('Shallow')

  form.resetField('nested.field.name')
  expect(form.state.values.nested.field.name).toEqual('Nested')
})

it('should reset nested fields', () => {
  const defaultValues = {
    shallow: '',
    nested: {
      field: {
        name: '',
      },
    },
  }

  const form = new FormApi({
    defaultValues,
  })
  form.mount()

  form.setFieldValue('shallow', 'Shallow')
  form.setFieldValue('nested.field.name', 'Nested')

  expect(form.state.values.shallow).toEqual('Shallow')
  expect(form.state.values.nested.field.name).toEqual('Nested')

  form.resetField('shallow')
  expect(form.state.values.shallow).toEqual('')

  form.resetField('nested.field.name')
  expect(form.state.values.nested.field.name).toEqual('')
})

it('should preserve nested fields on resetField if defaultValues is not provided', () => {
  const state = {
    shallow: '',
    nested: {
      field: {
        name: '',
      },
    },
  }

  const form = new FormApi({
    defaultState: { values: state },
  })
  form.mount()

  form.setFieldValue('shallow', 'Shallow')
  form.setFieldValue('nested.field.name', 'Nested')

  expect(form.state.values.shallow).toEqual('Shallow')
  expect(form.state.values.nested.field.name).toEqual('Nested')

  form.resetField('shallow')
  expect(form.state.values.shallow).toEqual('Shallow')

  form.resetField('nested.field.name')
  expect(form.state.values.nested.field.name).toEqual('Nested')
})

it('should accept formId and return it', () => {
  const form = new FormApi({
    defaultValues: { age: 0 },
    formId: 'age',
  })
  form.mount()

  expect(form.formId).toEqual('age')
})

it('should call onSubmitInvalid when submitted with onMount error', async () => {
  const onInvalidSpy = vi.fn()

  const form = new FormApi({
    defaultValues: { name: '' },
    validators: {
      onMount: () => ({ name: 'Name is required' }),
    },
    onSubmitInvalid: () => onInvalidSpy(),
  })
  form.mount()

  const field = new FieldApi({ form, name: 'name' })
  field.mount()

  expect(form.state.canSubmit).toBe(false)

  await form.handleSubmit()

  expect(onInvalidSpy).toHaveBeenCalledTimes(1)
})

it('should not run submit validation when canSubmit is false', async () => {
  const onSubmitValidatorSpy = vi
    .fn()
    .mockImplementation(() => 'Submit validation failed')
  const onInvalidSpy = vi.fn()

  const form = new FormApi({
    defaultValues: { name: '' },
    validators: {
      onMount: () => 'Name required',
      onSubmit: () => onSubmitValidatorSpy,
    },
    onSubmitInvalid: () => onInvalidSpy(),
  })
  form.mount()

  const field = new FieldApi({ form, name: 'name' })
  field.mount()

  expect(form.state.canSubmit).toBe(false)

  await form.handleSubmit()

  expect(onSubmitValidatorSpy).not.toHaveBeenCalled()
  expect(onInvalidSpy).toHaveBeenCalledTimes(1)
})

it('should respect canSubmitWhenInvalid option and run validation even when canSubmit is false', async () => {
  const onSubmitValidatorSpy = vi
    .fn()
    .mockImplementation(() => 'Submit validation failed')
  const onInvalidSpy = vi.fn()

  const form = new FormApi({
    defaultValues: { name: '' },
    canSubmitWhenInvalid: true,
    validators: {
      onMount: () => 'Name required',
      onSubmit: () => onSubmitValidatorSpy(),
    },
    onSubmitInvalid: () => onInvalidSpy(),
  })
  form.mount()

  const field = new FieldApi({ form, name: 'name' })
  field.mount()

  expect(form.state.canSubmit).toBe(true)

  await form.handleSubmit()

  expect(onSubmitValidatorSpy).toHaveBeenCalledTimes(1)
  expect(onInvalidSpy).toHaveBeenCalledTimes(1)
})

it('should generate a formId if not provided', () => {
  const form = new FormApi({
    defaultValues: { age: 0 },
  })
  form.mount()

  expect(form.formId.length).toBeGreaterThan(1)
})

describe('form api event client', () => {
  it('should have debug disabled', () => {
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})

    const form = new FormApi({
      defaultValues: {
        name: 'test',
      },
    })

    form.mount()

    expect(logSpy).not.toHaveBeenCalled()

    logSpy.mockRestore()
  })
})


--- packages/form-core/tests/FormApi.test-d.ts ---
import { expectTypeOf, it } from 'vitest'
import { z } from 'zod'
import { FormApi, formOptions } from '../src'
import type {
  DeepKeys,
  GlobalFormValidationError,
  StandardSchemaV1Issue,
  ValidationError,
  ValidationErrorMap,
} from '../src'

it('should return all errors matching the right type from getAllErrors', () => {
  const form = new FormApi({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    validators: {
      onChange: () => ['onChange'] as const,
      onMount: () => 10 as const,
      onBlur: () => ({ onBlur: true as const, onBlurNumber: 1 }),
      onSubmit: () => 'onSubmit' as const,
      onBlurAsync: () => Promise.resolve('onBlurAsync' as const),
      onChangeAsync: () => Promise.resolve('onChangeAsync' as const),
      onSubmitAsync: () => Promise.resolve('onSubmitAsync' as const),
    },
  })

  const errors = form.getAllErrors()

  errors.form.errorMap.onChange

  expectTypeOf(errors.form.errorMap).toEqualTypeOf<{
    onBlur?: { onBlur: true; onBlurNumber: number } | 'onBlurAsync'
    onChange?: readonly ['onChange'] | 'onChangeAsync'
    onMount?: 10
    onSubmit?: 'onSubmit' | 'onSubmitAsync'
    onServer?: undefined
    onDynamic?: undefined
  }>()

  expectTypeOf(errors.form.errors).toEqualTypeOf<
    (
      | readonly ['onChange']
      | 'onChangeAsync'
      | 10
      | 'onSubmit'
      | 'onSubmitAsync'
      | { onBlur: true; onBlurNumber: number }
      | 'onBlurAsync'
      | undefined
    )[]
  >()

  expectTypeOf(errors.fields).toEqualTypeOf<{
    firstName: {
      errors: ValidationError[]
      errorMap: ValidationErrorMap
    }
    lastName: {
      errors: ValidationError[]
      errorMap: ValidationErrorMap
    }
  }>()
})

it('should type handleSubmit as never when onSubmitMeta is not passed', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
  } as const)

  expectTypeOf(form.handleSubmit).toEqualTypeOf<{
    (): Promise<void>
    (submitMeta: never): Promise<void>
  }>()
})

type OnSubmitMeta = {
  group: string
}

it('should type handleChange correctly', () => {
  const form = new FormApi({
    defaultValues: {
      name: 'test',
    },
    onSubmitMeta: {} as OnSubmitMeta,
  } as const)

  form.handleSubmit({ group: 'track' })

  expectTypeOf(form.handleSubmit).toEqualTypeOf<{
    (): Promise<void>
    (submitMeta: OnSubmitMeta): Promise<void>
  }>()
})

type FormLevelStandardSchemaIssue = {
  form: Record<string, StandardSchemaV1Issue[]>
  fields: Record<string, StandardSchemaV1Issue[]>
}

it('should only have form-level error types returned from parseFieldValuesWithSchema and parseFieldValuesWithSchemaAsync', () => {
  const form = new FormApi({
    defaultValues: { name: '' },
  })
  form.mount()

  const schema = z.object({
    name: z.string(),
  })
  // assert that it doesn't think it's a field-level error
  expectTypeOf(form.parseValuesWithSchema(schema)).toEqualTypeOf<
    FormLevelStandardSchemaIssue | undefined
  >()
  expectTypeOf(form.parseValuesWithSchemaAsync(schema)).toEqualTypeOf<
    Promise<FormLevelStandardSchemaIssue | undefined>
  >()
})

it("should allow setting manual errors according to the validator's return type", () => {
  type FormData = {
    firstName: string
    lastName: string
  }

  const form = new FormApi({
    defaultValues: {} as FormData,
    validators: {
      onChange: () => ['onChange'] as const,
      onMount: () => 10 as const,
      onBlur: () => ({ onBlur: true as const, onBlurNumber: 1 }),
      onSubmit: () => 'onSubmit' as const,
      onBlurAsync: () => Promise.resolve('onBlurAsync' as const),
      onDynamic: () => 'onDynamic' as const,
      onDynamicAsync: () => Promise.resolve('onDynamicAsync' as const),
      onChangeAsync: () => Promise.resolve('onChangeAsync' as const),
      onSubmitAsync: () => Promise.resolve('onSubmitAsync' as const),
    },
  })

  form.setErrorMap({
    onMount: 10,
    onChange: ['onChange'],
  })

  expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{
    onMount: 10 | undefined | GlobalFormValidationError<FormData>
    onChange:
      | readonly ['onChange']
      | 'onChangeAsync'
      | undefined
      | GlobalFormValidationError<FormData>
    onBlur:
      | { onBlur: true; onBlurNumber: number }
      | 'onBlurAsync'
      | undefined
      | GlobalFormValidationError<FormData>
    onSubmit:
      | 'onSubmit'
      | 'onSubmitAsync'
      | undefined
      | GlobalFormValidationError<FormData>
    onDynamic:
      | 'onDynamic'
      | 'onDynamicAsync'
      | undefined
      | GlobalFormValidationError<FormData>
    onServer: undefined
  }>
})

it('should allow setting field errors from the global form error map', () => {
  type FormData = {
    firstName: string
    lastName: string
  }

  const form = new FormApi({
    defaultValues: {} as FormData,
  })

  form.setErrorMap({
    onChange: {
      fields: {
        firstName: 'error',
        // @ts-expect-error
        nonExistentField: 'error',
      },
    },
  })
})

it('should not allow setting manual errors if no validator is specified', () => {
  type FormData = {
    firstName: string
    lastName: string
  }
  const form = new FormApi({
    defaultValues: {} as FormData,
  })

  expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{
    onMount: undefined | GlobalFormValidationError<FormData>
    onChange: undefined | GlobalFormValidationError<FormData>
    onBlur: undefined | GlobalFormValidationError<FormData>
    onSubmit: undefined | GlobalFormValidationError<FormData>
    onDynamic: undefined | GlobalFormValidationError<FormData>
    onServer: undefined
  }>
})

it('should only allow array fields for array-specific methods', () => {
  type FormValues = {
    name: string
    age: number
    startDate: Date
    title: string | null | undefined
    relatives: { name: string }[]
    counts: (number | null | undefined)[]
  }

  const defaultValues: FormValues = {
    name: '',
    age: 0,
    startDate: new Date(),
    title: null,
    relatives: [{ name: '' }],
    counts: [5, null, undefined, 3],
  }

  const form = new FormApi({
    defaultValues,
  })
  form.mount()

  type AllKeys = DeepKeys<FormValues>
  type OnlyArrayKeys = Extract<AllKeys, 'counts' | 'relatives'>
  type RandomKeys = Extract<AllKeys, 'counts' | 'relatives' | 'title'>

  const push1 = form.pushFieldValue<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const push2 = form.pushFieldValue<AllKeys>
  // @ts-expect-error too wide!
  const push3 = form.pushFieldValue<RandomKeys>

  const insert1 = form.insertFieldValue<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const insert2 = form.insertFieldValue<AllKeys>
  // @ts-expect-error too wide!
  const insert3 = form.insertFieldValue<RandomKeys>

  const replace1 = form.replaceFieldValue<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const replace2 = form.replaceFieldValue<AllKeys>
  // @ts-expect-error too wide!
  const replace3 = form.replaceFieldValue<RandomKeys>

  const remove1 = form.removeFieldValue<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const remove2 = form.removeFieldValue<AllKeys>
  // @ts-expect-error too wide!
  const remove3 = form.removeFieldValue<RandomKeys>

  const swap1 = form.swapFieldValues<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const swap2 = form.swapFieldValues<AllKeys>
  // @ts-expect-error too wide!
  const swap3 = form.swapFieldValues<RandomKeys>

  const move1 = form.moveFieldValues<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const move2 = form.moveFieldValues<AllKeys>
  // @ts-expect-error too wide!
  const move3 = form.moveFieldValues<RandomKeys>

  const validate1 = form.validateArrayFieldsStartingFrom<OnlyArrayKeys>
  // @ts-expect-error too wide!
  const validate2 = form.validateArrayFieldsStartingFrom<AllKeys>
  // @ts-expect-error too wide!
  const validate3 = form.validateArrayFieldsStartingFrom<RandomKeys>
})

it('should infer full field name union for form.resetField parameters', () => {
  type FormData = {
    shallow: string
    nested: {
      field: {
        name: string
      }
    }
  }

  const defaultValue = {
    shallow: '',
    nested: {
      field: {
        name: '',
      },
    },
  }

  const form = new FormApi({
    defaultValues: defaultValue as FormData,
  })

  expectTypeOf(form.resetField)
    .parameter(0)
    .toEqualTypeOf<
      'shallow' | 'nested' | 'nested.field' | 'nested.field.name'
    >()
})

it('should extract the form error type from a global form error', () => {
  type FormData = {
    firstName: string
    lastName: string
  }

  const form = new FormApi({
    defaultValues: {} as FormData,
    validators: {
      onMount: () => {
        return {
          form: 'onMount' as const,
          fields: {},
        }
      },
      onChange: () => {
        return {
          form: 'onChange' as const,
          fields: {},
        }
      },
      onChangeAsync: () => {
        return Promise.resolve({
          form: 'onChangeAsync' as const,
          fields: {},
        })
      },
      onBlur: () => {
        return {
          form: 'onBlur' as const,
          fields: {},
        }
      },
      onBlurAsync: () => {
        return Promise.resolve('onBlurAsync' as const)
      },
    },
  })

  expectTypeOf(form.state.errorMap.onChange).toEqualTypeOf<
    'onChange' | 'onChangeAsync' | undefined
  >()

  expectTypeOf(form.state.errorMap.onMount).toEqualTypeOf<
    'onMount' | undefined
  >()

  expectTypeOf(form.state.errorMap.onBlur).toEqualTypeOf<
    'onBlur' | 'onBlurAsync' | undefined
  >()

  expectTypeOf(form.state.errors).toEqualTypeOf<
    (
      | 'onMount'
      | 'onChange'
      | 'onChangeAsync'
      | 'onBlur'
      | 'onBlurAsync'
      | undefined
    )[]
  >
})

it('listeners should be typed correctly', () => {
  type FormData = {
    firstName: string
    lastName: string
  }

  const form = new FormApi({
    defaultValues: {
      firstName: '',
      lastName: '',
    } as FormData,
    listeners: {
      onSubmit: ({ formApi }) => {
        expectTypeOf(formApi.state.values).toEqualTypeOf<FormData>()
      },
    },
  })

  form.handleSubmit()
})

it('listeners should be typed correctly when using formOptions', () => {
  type FormData = {
    firstName: string
    lastName: string
  }

  const formOpts = formOptions({
    defaultValues: {
      firstName: 'FirstName',
      lastName: 'LastName',
    } as FormData,
    validators: {
      onSubmit: () => {
        return {
          test: 'test',
        }
      },
    },
    listeners: {
      onSubmit: ({ formApi }) => {
        expectTypeOf(formApi.state.values).toEqualTypeOf<FormData>()
      },
    },
  })

  const form = new FormApi({
    ...formOpts,
    listeners: {
      // this doesn't error since listeners return void
      onSubmit: ({ formApi }) => {
        console.log(formApi.state.values)
      },
    },
    validators: {
      // this errors becuase the return type is not the same as the validator return type
      // onSubmit: () => 'custom on submit',

      onSubmit: () => {
        return {
          test: 'can change the value!',
        }
      },
      onChange: () => {
        return 'onChange'
      },
    },
  })

  form.handleSubmit()
})


--- CONTRIBUTING.md ---
# Contributing

## Questions

If you have questions about implementation details, help or support, then please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/form/discussions) **PLEASE NOTE:** If you choose to instead open an issue for your question, your issue will be immediately closed and redirected to the forum.

## Reporting Issues

If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/form/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/form/discussions)

## Suggesting new features

If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented.

## Development

If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started:

- Fork this repository.
- Install dependencies

  ```bash
  pnpm install
  ```

  - We use [pnpm](https://pnpm.io/) v9 for package management (run in case of pnpm-related issues).

    ```bash
    corepack enable && corepack prepare
    ```

  - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc`

    ```bash
    nvm use
    ```

- Build all packages.

  ```bash
  pnpm build:all
  ```

- Run development server.

  ```bash
  pnpm run watch
  ```

- Implement your changes and tests to files in the `src/` directory and corresponding test files.
- Document your changes in the appropriate doc page.
- Git stage your required changes and commit (see below commit guidelines).
- Submit PR for review.

### Editing the docs locally and previewing the changes

The documentations for all the TanStack projects are hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/form` docs.

> [!NOTE]
> The website fetches the doc pages from GitHub in production, and searches for them at `../form/docs` in development. Your local clone of `TanStack/form` needs to be in the same directory as the local clone of `TansStack/tanstack.com`.

You can follow these steps to set up the docs for local development:

1. Make a new directory called `tanstack`.

```sh
mkdir tanstack
```

2. Enter that directory and clone the [`TanStack/form`](https://github.com/TanStack/form) and [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com) repos.

```sh
cd tanstack
git clone git@github.com:TanStack/form.git
# We probably don't need all the branches and commit history
# from the `tanstack.com` repo, so let's just create a shallow
# clone of the latest version of the `main` branch.
# Read more about shallow clones here:
# https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-shallow-clones
git clone git@github.com:TanStack/tanstack.com.git --depth=1 --single-branch --branch=main
```

> [!NOTE]
> Your `tanstack` directory should look like this:
>
> ```
> tanstack/
>    |
>    +-- form/ (<-- this directory cannot be called anything else!)
>    |
>    +-- tanstack.com/
> ```

3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode:

```sh
cd tanstack.com
pnpm i
# The app will run on https://localhost:3000 by default
pnpm dev
```

4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs` there.

> [!WARNING]
> You will need to update the `docs/config.json` file (in `TanStack/form`) if you add a new documentation page!

You can see the whole process in the screen capture below:

https://github.com/fulopkovacs/form/assets/43729152/9d35a3c3-8153-4e74-9cb2-af275f7a269b

### Running examples

- Make sure you've installed the dependencies in the repo's root directory.

  ```bash
  pnpm install
  ```

- If you want to run the example against your local changes, run below in the repo's root directory. Otherwise, it will be run against the latest TanStack Form release.

  ```bash
  pnpm run watch
  ```

- Run below in the selected examples' directory.

  ```bash
  pnpm run dev
  ```

#### Note on standalone execution

If you want to run an example without installing dependencies for the whole repo, just follow instructions from the example's README.md file. It will be then run against the latest TanStack Form release.

## Online one-click setup

You can use Gitpod (An Online Open Source VS Code like IDE which is free for Open Source) for developing online. With a single click it will start a workspace and automatically:

- clone the `TanStack/form` repo.
- install all the dependencies in `/` and `/docs`.
- run below in the root(`/`) to Auto-build files.

  ```bash
  npm start
  ```

- run below in `/docs`.

  ```bash
  npm run dev
  ```

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TanStack/form)

## Commit message conventions

`TanStack/form` is using [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines).

We have very precise rules over how our git commit messages can be formatted. This leads to **more readable messages** that are easy to follow when looking through the **project history**.

### Commit Message Format

Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:

```text
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```

The **header** is mandatory and the **scope** of the header is optional.

Any line of the commit message cannot be longer than 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools.

### Type

Must be one of the following:

- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing or correcting existing tests
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation

### Scope

The scope could be anything specifying place of the commit change. For example `form-core`, `react-form` etc...

You can use `*` when the change affects more than a single scope.

### Subject

The subject contains succinct description of the change:

- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize first letter
- no dot (.) at the end

### Body

Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.

### Footer

The footer should contain any information about **Breaking Changes** and is also the place to [reference GitHub issues that this commit closes](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue).

**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.

### Example

Here is an example of the release type that will be done based on a commit messages:

| Commit message                                                                                                                                                                                    | Release type               |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| fix(pencil): stop graphite breaking when too much pressure applied                                                                                                                                | Patch Release              |
| feat(pencil): add `graphiteWidth` option                                                                                                                                                          | ~~Minor~~ Feature Release  |
| perf(pencil): remove `graphiteWidth` option<br/><br/>BREAKING CHANGE: The `graphiteWidth` option has been removed.<br/>The default graphite width of 10mm is always used for performance reasons. | ~~Major~~ Breaking Release |

### Revert

If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.

## Pull requests

Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface.

Use an appropriate commit type. Be especially careful with breaking changes.

## Releases

For each new commit added to `main` with `git push` or by merging a pull request or merging from another branch, a GitHub action is triggered and runs the `semantic-release` command to make a release if there are codebase changes since the last release that affect the package functionalities.


## Links discovered
- [GitHub Discussions](https://github.com/TanStack/form/discussions)
- [file an issue](https://github.com/TanStack/form/issues/new/choose)
- [pnpm](https://pnpm.io/)
- [nvm](https://github.com/nvm-sh/nvm)
- [tanstack.com](https://tanstack.com)
- [`TanStack/form`](https://github.com/TanStack/form)
- [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com)
- [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)
- [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines)
- [reference GitHub issues that this commit closes](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue)

--- packages/angular-form/CHANGELOG.md ---
# @tanstack/angular-form

## 1.27.3

### Patch Changes

- Updated dependencies [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)]:
  - @tanstack/form-core@1.27.3

## 1.27.2

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-core@1.27.2

## 1.27.1

### Patch Changes

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/form-core@1.27.1

## 1.27.0

### Patch Changes

- Updated dependencies [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)]:
  - @tanstack/form-core@1.27.0

## 1.26.0

### Patch Changes

- Updated dependencies [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)]:
  - @tanstack/form-core@1.26.0

## 1.25.0

### Patch Changes

- Updated dependencies [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)]:
  - @tanstack/form-core@1.25.0

## 1.23.9

### Patch Changes

- Updated dependencies [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)]:
  - @tanstack/form-core@1.24.5

## 1.23.8

### Patch Changes

- form-core: Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758))

- Updated dependencies [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)]:
  - @tanstack/form-core@1.24.4

## 1.23.7

### Patch Changes

- Updated dependencies [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)]:
  - @tanstack/form-core@1.24.3: respect dontValidate option in formApi array modifiers ([#1775](https://github.com/TanStack/form/pull/1775))

## 1.23.6

### Patch Changes

- Updated dependencies [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)]:
  - @tanstack/form-core@1.24.2: prevent runtime errors when using `deleteField` ([#1706](https://github.com/TanStack/form/pull/1706))

## 1.23.5

### Patch Changes

- Updated dependencies [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)]:
  - @tanstack/form-core@1.24.1

## 1.23.4

### Patch Changes

- Updated dependencies [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)]:
  - @tanstack/form-core@1.24.0

## 1.23.3

### Patch Changes

- Updated dependencies [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)]:
  - @tanstack/form-core@1.23.3

## 1.23.2

### Patch Changes

- Updated dependencies [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)]:
  - @tanstack/form-core@1.23.2

## 1.23.1

### Patch Changes

- Updated dependencies [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)]:
  - @tanstack/form-core@1.23.1

## 1.23.0

### Patch Changes

- Updated dependencies [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f), [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)]:
  - @tanstack/form-core@1.23.0

## 1.21.1

### Patch Changes

- Updated dependencies [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)]:
  - @tanstack/form-core@1.22.0


## Links discovered
- [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)
- [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)
- [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)
- [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)
- [#1758](https://github.com/TanStack/form/pull/1758)
- [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)
- [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)
- [#1775](https://github.com/TanStack/form/pull/1775)
- [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)
- [#1706](https://github.com/TanStack/form/pull/1706)
- [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)
- [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)
- [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)
- [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)
- [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)
- [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f)
- [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)
- [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)

--- packages/form-core/CHANGELOG.md ---
# @tanstack/form-core

## 1.27.3

### Patch Changes

- Bump TanStack pacer to pacer-lite for reduced custom event emissions. ([#1876](https://github.com/TanStack/form/pull/1876))

## 1.27.2

## 1.27.1

### Patch Changes

- Fix issues with methods not being present in React adapter ([#1903](https://github.com/TanStack/form/pull/1903))

## 1.27.0

### Patch Changes

- Fixed issues with React Compiler ([#1893](https://github.com/TanStack/form/pull/1893))

- Fix issue with deleteField and numeric keys ([#1891](https://github.com/TanStack/form/pull/1891))

## 1.26.0

### Patch Changes

- fix stale fields on array changes ([#1729](https://github.com/TanStack/form/pull/1729))
- allow explicitly setting `field.handleChange(undefined)` ([#1729](https://github.com/TanStack/form/pull/1729))

## 1.25.0

### Patch Changes

- Removes debug config from event client in form-core ([#1852](https://github.com/TanStack/form/pull/1852))

## 1.24.5

### Patch Changes

- - Make `fieldMeta` record type `Partial<>` to reflect runtime behaviour ([#1787](https://github.com/TanStack/form/pull/1787))

## 1.24.4

### Patch Changes

- Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758))

## 1.24.3

### Patch Changes

- respect dontValidate option in formApi array modifiers ([#1775](https://github.com/TanStack/form/pull/1775))

## 1.24.2

### Patch Changes

- fix(form-core): prevent runtime errors when using `deleteField` ([#1706](https://github.com/TanStack/form/pull/1706))

## 1.24.1

### Patch Changes

- fix(form-core): call `onSubmitInvalid` even when `canSubmit` is false ([#1697](https://github.com/TanStack/form/pull/1697))

## 1.24.0

### Minor Changes

- Removes UUID from package.json for native environments. Reverts formId to a getter function. ([#1753](https://github.com/TanStack/form/pull/1753))

## 1.23.3

### Patch Changes

- Bump @tanstack/devtools-event-client to 0.3.2, patches side effects in event client. ([#1767](https://github.com/TanStack/form/pull/1767))

## 1.23.2

### Patch Changes

- fix(form-core): handle string array indices in prefixSchemaToErrors ([#1689](https://github.com/TanStack/form/pull/1689))

## 1.23.1

### Patch Changes

- bump to latest event client, for angular ssr ([#1761](https://github.com/TanStack/form/pull/1761))

## 1.23.0

### Minor Changes

- ssr, dayjs, uuid, version bump patch ([#1747](https://github.com/TanStack/form/pull/1747))

- Jumping v1.22.0 as it's incorrectly published, fixed adapter to core. ([#1749](https://github.com/TanStack/form/pull/1749))

## 1.22.0

### Minor Changes

- Bump core to match devtools, docs config update ([#1739](https://github.com/TanStack/form/pull/1739))


## Links discovered
- [#1876](https://github.com/TanStack/form/pull/1876)
- [#1903](https://github.com/TanStack/form/pull/1903)
- [#1893](https://github.com/TanStack/form/pull/1893)
- [#1891](https://github.com/TanStack/form/pull/1891)
- [#1729](https://github.com/TanStack/form/pull/1729)
- [#1852](https://github.com/TanStack/form/pull/1852)
- [#1787](https://github.com/TanStack/form/pull/1787)
- [#1758](https://github.com/TanStack/form/pull/1758)
- [#1775](https://github.com/TanStack/form/pull/1775)
- [#1706](https://github.com/TanStack/form/pull/1706)
- [#1697](https://github.com/TanStack/form/pull/1697)
- [#1753](https://github.com/TanStack/form/pull/1753)
- [#1767](https://github.com/TanStack/form/pull/1767)
- [#1689](https://github.com/TanStack/form/pull/1689)
- [#1761](https://github.com/TanStack/form/pull/1761)
- [#1747](https://github.com/TanStack/form/pull/1747)
- [#1749](https://github.com/TanStack/form/pull/1749)
- [#1739](https://github.com/TanStack/form/pull/1739)

--- packages/form-devtools/CHANGELOG.md ---
# @tanstack/form-devtools

## 0.2.6

### Patch Changes

- Updated dependencies [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)]:
  - @tanstack/form-core@1.27.3

## 0.2.5

### Patch Changes

- Update @tanstack/devtools-utils to 0.0.9, fixes react 17 conflict ([#1892](https://github.com/TanStack/form/pull/1892))

- Updated dependencies []:
  - @tanstack/form-core@1.27.2

## 0.2.4

### Patch Changes

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/form-core@1.27.1

## 0.2.3

### Patch Changes

- Updated dependencies [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)]:
  - @tanstack/form-core@1.27.0

## 0.2.2

### Patch Changes

- Updated dependencies [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)]:
  - @tanstack/form-core@1.26.0

## 0.2.1

### Patch Changes

- Updated dependencies [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)]:
  - @tanstack/form-core@1.25.0

## 0.2.0

### Minor Changes

- Migrated to devtools utils, adds support for solid devtools. Renamed plugin to reflect tanstack plugin schema (FormDevtoolsPlugin to formDevtoolsPlugin) ([#1789](https://github.com/TanStack/form/pull/1789))

### Patch Changes

- Updated dependencies [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)]:
  - @tanstack/form-core@1.24.5

## 0.1.8

### Patch Changes

- form-core: Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758))

- Updated dependencies [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)]:
  - @tanstack/form-core@1.24.4

## 0.1.7

### Patch Changes

- Updated dependencies [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)]:
  - @tanstack/form-core@1.24.3: respect dontValidate option in formApi array modifiers ([#1775](https://github.com/TanStack/form/pull/1775))

## 0.1.6

### Patch Changes

- Updated dependencies [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)]:
  - @tanstack/form-core@1.24.2: prevent runtime errors when using `deleteField` ([#1706](https://github.com/TanStack/form/pull/1706))

## 0.1.5

### Patch Changes

- Updated dependencies [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)]:
  - @tanstack/form-core@1.24.1

## 0.1.4

### Patch Changes

- Updated dependencies [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)]:
  - @tanstack/form-core@1.24.0

## 0.1.3

### Patch Changes

- Updated dependencies [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)]:
  - @tanstack/form-core@1.23.3

## 0.1.2

### Patch Changes

- Updated dependencies [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)]:
  - @tanstack/form-core@1.23.2

## 0.1.1

### Patch Changes

- Updated dependencies [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)]:
  - @tanstack/form-core@1.23.1

## 0.1.0

### Minor Changes

- ssr, dayjs, uuid, version bump patch ([#1747](https://github.com/TanStack/form/pull/1747))

### Patch Changes

- Jumping v1.22.0 as it's incorrectly published, fixed adapter to core. ([#1749](https://github.com/TanStack/form/pull/1749))

- Updated dependencies [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f), [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)]:
  - @tanstack/form-core@1.23.0

## 0.0.2

### Patch Changes

- Updated dependencies [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)]:
  - @tanstack/form-core@1.22.0


## Links discovered
- [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)
- [#1892](https://github.com/TanStack/form/pull/1892)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)
- [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)
- [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)
- [#1789](https://github.com/TanStack/form/pull/1789)
- [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)
- [#1758](https://github.com/TanStack/form/pull/1758)
- [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)
- [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)
- [#1775](https://github.com/TanStack/form/pull/1775)
- [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)
- [#1706](https://github.com/TanStack/form/pull/1706)
- [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)
- [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)
- [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)
- [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)
- [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)
- [#1747](https://github.com/TanStack/form/pull/1747)
- [#1749](https://github.com/TanStack/form/pull/1749)
- [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f)
- [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)
- [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)

--- packages/lit-form/CHANGELOG.md ---
# @tanstack/lit-form

## 1.23.15

### Patch Changes

- Updated dependencies [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)]:
  - @tanstack/form-core@1.27.3

## 1.23.14

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-core@1.27.2

## 1.23.13

### Patch Changes

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/form-core@1.27.1

## 1.23.12

### Patch Changes

- Updated dependencies [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)]:
  - @tanstack/form-core@1.27.0

## 1.23.11

### Patch Changes

- Updated dependencies [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)]:
  - @tanstack/form-core@1.26.0

## 1.23.10

### Patch Changes

- Updated dependencies [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)]:
  - @tanstack/form-core@1.25.0

## 1.23.9

### Patch Changes

- Updated dependencies [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)]:
  - @tanstack/form-core@1.24.5

## 1.23.8

### Patch Changes

- form-core: Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758))

- Updated dependencies [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)]:
  - @tanstack/form-core@1.24.4

## 1.23.7

### Patch Changes

- Updated dependencies [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)]:
  - @tanstack/form-core@1.24.3: respect dontValidate option in formApi array modifiers ([#1775](https://github.com/TanStack/form/pull/1775))

## 1.23.6

### Patch Changes

- Updated dependencies [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)]:
  - @tanstack/form-core@1.24.2: prevent runtime errors when using `deleteField` ([#1706](https://github.com/TanStack/form/pull/1706))

## 1.23.5

### Patch Changes

- Updated dependencies [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)]:
  - @tanstack/form-core@1.24.1

## 1.23.4

### Patch Changes

- Updated dependencies [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)]:
  - @tanstack/form-core@1.24.0

## 1.23.3

### Patch Changes

- Updated dependencies [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)]:
  - @tanstack/form-core@1.23.3

## 1.23.2

### Patch Changes

- Updated dependencies [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)]:
  - @tanstack/form-core@1.23.2

## 1.23.1

### Patch Changes

- Updated dependencies [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)]:
  - @tanstack/form-core@1.23.1

## 1.23.0

### Patch Changes

- Updated dependencies [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f), [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)]:
  - @tanstack/form-core@1.23.0

## 1.21.1

### Patch Changes

- Updated dependencies [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)]:
  - @tanstack/form-core@1.22.0


## Links discovered
- [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)
- [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)
- [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)
- [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)
- [#1758](https://github.com/TanStack/form/pull/1758)
- [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)
- [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)
- [#1775](https://github.com/TanStack/form/pull/1775)
- [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)
- [#1706](https://github.com/TanStack/form/pull/1706)
- [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)
- [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)
- [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)
- [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)
- [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)
- [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f)
- [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)
- [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)

--- packages/react-form-devtools/CHANGELOG.md ---
# @tanstack/react-form-devtools

## 0.2.6

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.2.6

## 0.2.5

### Patch Changes

- Updated dependencies [[`e18f00c`](https://github.com/TanStack/form/commit/e18f00c0aaf14c879f304c2177539232ffa2e76b)]:
  - @tanstack/form-devtools@0.2.5

## 0.2.4

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.2.4

## 0.2.3

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.2.3

## 0.2.2

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.2.2

## 0.2.1

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.2.1

## 0.2.0

### Minor Changes

- Migrated to devtools utils, adds support for solid devtools. Renamed plugin to reflect tanstack plugin schema (FormDevtoolsPlugin to formDevtoolsPlugin) ([#1789](https://github.com/TanStack/form/pull/1789))

### Patch Changes

- Updated dependencies [[`154ac18`](https://github.com/TanStack/form/commit/154ac183a63533227abc67838ca2ca5385a5551d)]:
  - @tanstack/form-devtools@0.2.0

## 0.1.8

### Patch Changes

- form-devtools: Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758))

- Updated dependencies [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)]:
  - @tanstack/form-devtools@0.1.8

## 0.1.7

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.7

## 0.1.6

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.6

## 0.1.5

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.5

## 0.1.4

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.4

## 0.1.3

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.3

## 0.1.2

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.2

## 0.1.1

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.1.1

## 0.1.0

### Minor Changes

- ssr, dayjs, uuid, version bump patch ([#1747](https://github.com/TanStack/form/pull/1747))

### Patch Changes

- Updated dependencies [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f), [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)]:
  - @tanstack/form-devtools@0.1.0

## 0.0.2

### Patch Changes

- Updated dependencies []:
  - @tanstack/form-devtools@0.0.2


## Links discovered
- [[`e18f00c`](https://github.com/TanStack/form/commit/e18f00c0aaf14c879f304c2177539232ffa2e76b)
- [#1789](https://github.com/TanStack/form/pull/1789)
- [[`154ac18`](https://github.com/TanStack/form/commit/154ac183a63533227abc67838ca2ca5385a5551d)
- [#1758](https://github.com/TanStack/form/pull/1758)
- [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)
- [#1747](https://github.com/TanStack/form/pull/1747)
- [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f)
- [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)

--- packages/react-form-nextjs/CHANGELOG.md ---
# @tanstack/react-form-nextjs

## 1.27.3

### Patch Changes

- Updated dependencies []:
  - @tanstack/react-form@1.27.3

## 1.27.2

### Patch Changes

- use React 18's useId hook by default for formId generation, only calling Math.random() as a fallback if no formId is provided. ([#1913](https://github.com/TanStack/form/pull/1913))

- Updated dependencies [[`6a5e1c1`](https://github.com/TanStack/form/commit/6a5e1c1a4f0f0519705cc5fd15cbe8afb878a42d), [`9ce5f28`](https://github.com/TanStack/form/commit/9ce5f286a12fb0f1662f3eeeff14c75d8f3e4417)]:
  - @tanstack/react-form@1.27.2

## 1.27.1

### Patch Changes

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/react-form@1.27.1

## 1.27.0

### Patch Changes

- Updated dependencies [[`03c2bee`](https://github.com/TanStack/form/commit/03c2beed867f097ac61fb6411ce9cd5a9f3b4c58), [`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4cd4068`](https://github.com/TanStack/form/commit/4cd4068014ee902399da6978becd443068b0c672)]:
  - @tanstack/react-form@1.27.0

## 1.26.0

### Patch Changes

- Updated dependencies []:
  - @tanstack/react-form@1.26.0

## 1.25.0

### Patch Changes

- Updated dependencies [[`d3883e9`](https://github.com/TanStack/form/commit/d3883e9c408aff97a24b7066145b6dea41ae304a)]:
  - @tanstack/react-form@1.25.0


## Links discovered
- [#1913](https://github.com/TanStack/form/pull/1913)
- [[`6a5e1c1`](https://github.com/TanStack/form/commit/6a5e1c1a4f0f0519705cc5fd15cbe8afb878a42d)
- [`9ce5f28`](https://github.com/TanStack/form/commit/9ce5f286a12fb0f1662f3eeeff14c75d8f3e4417)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [[`03c2bee`](https://github.com/TanStack/form/commit/03c2beed867f097ac61fb6411ce9cd5a9f3b4c58)
- [`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4cd4068`](https://github.com/TanStack/form/commit/4cd4068014ee902399da6978becd443068b0c672)
- [[`d3883e9`](https://github.com/TanStack/form/commit/d3883e9c408aff97a24b7066145b6dea41ae304a)

--- packages/react-form-remix/CHANGELOG.md ---
# @tanstack/react-form-remix

## 1.27.3

### Patch Changes

- Updated dependencies []:
  - @tanstack/react-form@1.27.3

## 1.27.2

### Patch Changes

- Updated dependencies [[`6a5e1c1`](https://github.com/TanStack/form/commit/6a5e1c1a4f0f0519705cc5fd15cbe8afb878a42d), [`9ce5f28`](https://github.com/TanStack/form/commit/9ce5f286a12fb0f1662f3eeeff14c75d8f3e4417)]:
  - @tanstack/react-form@1.27.2

## 1.27.1

### Patch Changes

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/react-form@1.27.1

## 1.27.0

### Patch Changes

- Updated dependencies [[`03c2bee`](https://github.com/TanStack/form/commit/03c2beed867f097ac61fb6411ce9cd5a9f3b4c58), [`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4cd4068`](https://github.com/TanStack/form/commit/4cd4068014ee902399da6978becd443068b0c672)]:
  - @tanstack/react-form@1.27.0

## 1.26.0

### Patch Changes

- Updated dependencies []:
  - @tanstack/react-form@1.26.0

## 1.25.0

### Patch Changes

- Updated dependencies [[`d3883e9`](https://github.com/TanStack/form/commit/d3883e9c408aff97a24b7066145b6dea41ae304a)]:
  - @tanstack/react-form@1.25.0


## Links discovered
- [[`6a5e1c1`](https://github.com/TanStack/form/commit/6a5e1c1a4f0f0519705cc5fd15cbe8afb878a42d)
- [`9ce5f28`](https://github.com/TanStack/form/commit/9ce5f286a12fb0f1662f3eeeff14c75d8f3e4417)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [[`03c2bee`](https://github.com/TanStack/form/commit/03c2beed867f097ac61fb6411ce9cd5a9f3b4c58)
- [`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4cd4068`](https://github.com/TanStack/form/commit/4cd4068014ee902399da6978becd443068b0c672)
- [[`d3883e9`](https://github.com/TanStack/form/commit/d3883e9c408aff97a24b7066145b6dea41ae304a)

--- packages/react-form-start/CHANGELOG.md ---
# @tanstack/react-form-start

## 1.27.3

### Patch Changes

- Updated dependencies []:
  - @tanstack/react-form@1.27.3

## 1.27.2

### Patch Changes

- Updated dependencies [[`6a5e1c1`](https://github.com/TanStack/form/commit/6a5e1c1a4f0f0519705cc5fd15cbe8afb878a42d), [`9ce5f28`](https://github.com/TanStack/form/commit/9ce5f286a12fb0f1662f3eeeff14c75d8f3e4417)]:
  - @tanstack/react-form@1.27.2

## 1.27.1

### Patch Changes

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/react-form@1.27.1

## 1.27.0

### Patch Changes

- Updated dependencies [[`03c2bee`](https://github.com/TanStack/form/commit/03c2beed867f097ac61fb6411ce9cd5a9f3b4c58), [`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4cd4068`](https://github.com/TanStack/form/commit/4cd4068014ee902399da6978becd443068b0c672)]:
  - @tanstack/react-form@1.27.0

## 1.26.0

### Patch Changes

- Updated dependencies []:
  - @tanstack/react-form@1.26.0

## 1.25.0

### Patch Changes

- Updated dependencies [[`d3883e9`](https://github.com/TanStack/form/commit/d3883e9c408aff97a24b7066145b6dea41ae304a)]:
  - @tanstack/react-form@1.25.0


## Links discovered
- [[`6a5e1c1`](https://github.com/TanStack/form/commit/6a5e1c1a4f0f0519705cc5fd15cbe8afb878a42d)
- [`9ce5f28`](https://github.com/TanStack/form/commit/9ce5f286a12fb0f1662f3eeeff14c75d8f3e4417)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [[`03c2bee`](https://github.com/TanStack/form/commit/03c2beed867f097ac61fb6411ce9cd5a9f3b4c58)
- [`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4cd4068`](https://github.com/TanStack/form/commit/4cd4068014ee902399da6978becd443068b0c672)
- [[`d3883e9`](https://github.com/TanStack/form/commit/d3883e9c408aff97a24b7066145b6dea41ae304a)

--- packages/react-form/CHANGELOG.md ---
# @tanstack/react-form

## 1.27.3

### Patch Changes

- Updated dependencies [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)]:
  - @tanstack/form-core@1.27.3

## 1.27.2

### Patch Changes

- use React 18's useId hook by default for formId generation, only calling Math.random() as a fallback if no formId is provided. ([#1913](https://github.com/TanStack/form/pull/1913))

- fix(react-form): ensure `FormApi.handleSubmit` returns a promise again ([#1924](https://github.com/TanStack/form/pull/1924))

- Updated dependencies []:
  - @tanstack/form-core@1.27.2

## 1.27.1

### Patch Changes

- Fix issues with methods not being present in React adapter ([#1903](https://github.com/TanStack/form/pull/1903))

- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]:
  - @tanstack/form-core@1.27.1

## 1.27.0

### Patch Changes

- Minorly improve performance and fix issues with Start ([#1882](https://github.com/TanStack/form/pull/1882))

- Fixed issues with React Compiler ([#1893](https://github.com/TanStack/form/pull/1893))

- Remove useId for react 17 user compatibility, replaced with uuid ([#1850](https://github.com/TanStack/form/pull/1850))

- Updated dependencies [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)]:
  - @tanstack/form-core@1.27.0

## 1.26.0

### Patch Changes

- Updated dependencies [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)]:
  - @tanstack/form-core@1.26.0

## 1.25.0

### Minor Changes

- Update Start to Release Candidate version. Extracted start, remix and nextJs adapters to the respective libraries @tanstack/react-form-start, @tanstack/react-form-remix, and @tanstack/react-form-nextjs, ([#1771](https://github.com/TanStack/form/pull/1771))

### Patch Changes

- Updated dependencies [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)]:
  - @tanstack/form-core@1.25.0

## 1.23.9

### Patch Changes

- Updated dependencies [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)]:
  - @tanstack/form-core@1.24.5

## 1.23.8

### Patch Changes

- Allow interfaces to be assigned to `withFieldGroup`'s `props`. ([#1816](https://github.com/TanStack/form/pull/1816))

- Allow returning all other `ReactNode`s not just `JSX.Element` in the `render` function of `withForm` and `withFieldGroup`. ([#1817](https://github.com/TanStack/form/pull/1817))

- form-core: Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758))

- Updated dependencies [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)]:
  - @tanstack/form-core@1.24.4

## 1.23.7

### Patch Changes

- Updated dependencies [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)]:
  - @tanstack/form-core@1.24.3: respect dontValidate option in formApi array modifiers ([#1775](https://github.com/TanStack/form/pull/1775))

## 1.23.6

### Patch Changes

- Updated dependencies [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)]:
  - @tanstack/form-core@1.24.2: prevent runtime errors when using `deleteField` ([#1706](https://github.com/TanStack/form/pull/1706))

## 1.23.5

### Patch Changes

- Updated dependencies [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)]:
  - @tanstack/form-core@1.24.1

## 1.23.4

### Patch Changes

- Updated dependencies [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)]:
  - @tanstack/form-core@1.24.0

## 1.23.3

### Patch Changes

- Updated dependencies [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)]:
  - @tanstack/form-core@1.23.3

## 1.23.2

### Patch Changes

- Updated dependencies [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)]:
  - @tanstack/form-core@1.23.2

## 1.23.1

### Patch Changes

- Updated dependencies [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)]:
  - @tanstack/form-core@1.23.1

## 1.23.0

### Patch Changes

- Updated dependencies [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f), [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)]:
  - @tanstack/form-core@1.23.0

## 1.21.1

### Patch Changes

- Updated dependencies [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)]:
  - @tanstack/form-core@1.22.0


## Links discovered
- [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)
- [#1913](https://github.com/TanStack/form/pull/1913)
- [#1924](https://github.com/TanStack/form/pull/1924)
- [#1903](https://github.com/TanStack/form/pull/1903)
- [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)
- [#1882](https://github.com/TanStack/form/pull/1882)
- [#1893](https://github.com/TanStack/form/pull/1893)
- [#1850](https://github.com/TanStack/form/pull/1850)
- [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1)
- [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)
- [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)
- [#1771](https://github.com/TanStack/form/pull/1771)
- [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)
- [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)
- [#1816](https://github.com/TanStack/form/pull/1816)
- [#1817](https://github.com/TanStack/form/pull/1817)
- [#1758](https://github.com/TanStack/form/pull/1758)
- [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)
- [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)
- [#1775](https://github.com/TanStack/form/pull/1775)
- [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)
- [#1706](https://github.com/TanStack/form/pull/1706)
- [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)
- [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)
- [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)
- [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)
- [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)
- [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f)
- [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)
- [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)

--- .github/pull_request_template.md ---
## 🎯 Changes

<!-- What changes are made in this PR? Describe the change and its motivation. -->

## ✅ Checklist

- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/form/blob/main/CONTRIBUTING.md).
- [ ] I have tested this code locally with `pnpm test:pr`.

## 🚀 Release Impact

- [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
- [ ] This change is docs/CI/dev-only (no release).


## Links discovered
- [Contributing guide](https://github.com/TanStack/form/blob/main/CONTRIBUTING.md)
- [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)

--- README.md ---
<div align="center">
  <img src="./media/header_form.png" >
</div>

<br />

<div align="center">
	<a href="https://www.npmjs.com/package/@tanstack/form-core" target="_parent">
  <img alt="NPM downloads for @tanstack/form-core" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a>
<a href="https://github.com/TanStack/form/" target="_parent">
  <img alt="Star TanStack Form on GitHub" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a>
	<a href="https://bundlephobia.com/package/@tanstack/form-core@latest" target="_parent">
  <img alt="Minified + gzipped bundle size of @tanstack/form-core" src="https://badgen.net/bundlephobia/minzip/@tanstack/form-core" />
</a>
</div>

<div align="center">
<a href="#badge">
  <img alt="Semantic Release Enabled" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
	<a href="https://bestofjs.org/projects/tanstack-form">
  <img alt="TanStack Form featured on Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%2Fform%26since=daily" />
</a>
	<a href="https://twitter.com/tan_stack">
		<img src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social" alt="Follow @TanStack"/>
	</a>
</div>

<div align="center">
  
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
</div>

# TanStack Form

A headless form library for managing complex form state with full control over fields, validation, and workflows across any framework.

- Framework‑agnostic & headless — bring your own UI
- Fully typed with TypeScript
- Reactive hooks & extensible modular architecture
- Sync & async validation with debouncing and nested fields

### <a href="https://tanstack.com/form">Read the docs →</b></a>

<br />

## Get Involved

- We welcome issues and pull requests!
- Participate in [GitHub discussions](https://github.com/TanStack/form/discussions)
- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions

## Partners

<table align="center">
  <tr>
    <td>
        <a href="https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS">
			<picture>
			  <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" />
			  <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" />
			  <img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" />
			</picture>        
		</a>
    </td>
    <td padding="20">
      <a href="https://www.cloudflare.com?utm_source=tanstack">
         <picture>
		  <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" />
		  <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" />
		  <img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" />
		</picture>
      </a>
    </td>
  </tr>
</table>

<div align="center">
<img src="./media/partner_logo.svg" alt="Form & you?" height="65">
<p>
We're looking for TanStack Form Partners to join our mission! Partner with us to push the boundaries of TanStack Form and build amazing things together.
</p>
<a href="mailto:partners@tanstack.com?subject=TanStack Form Partnership"><b>LET'S CHAT</b></a>
</div>

</div>

## Explore the TanStack Ecosystem

- <a href="https://github.com/tanstack/config"><b>TanStack Config</b></a> – Tooling for JS/TS packages
- <a href="https://github.com/tanstack/db"><b>TanStack DB</b></a> – Reactive sync client store
- <a href="https://github.com/tanstack/devtools"><b>TanStack DevTools</b></a> – Unified devtools panel
- <a href="https://github.com/tanstack/pacer"><b>TanStack Pacer</b></a> – Debouncing, throttling, batching <br/>
- <a href="https://github.com/tanstack/query"><b>TanStack Query</b></a> – Async state & caching
- <a href="https://github.com/tanstack/ranger"><b>TanStack Ranger</b></a> – Range & slider primitives
- <a href="https://github.com/tanstack/router"><b>TanStack Router</b></a> – Type‑safe routing, caching & URL state
- <a href="https://github.com/tanstack/router"><b>TanStack Start</b></a> – Full‑stack SSR & streaming
- <a href="https://github.com/tanstack/store"><b>TanStack Store</b></a> – Reactive data store
- <a href="https://github.com/tanstack/table"><b>TanStack Table</b></a> – Headless datagrids
- <a href="https://github.com/tanstack/virtual"><b>TanStack Virtual</b></a> – Virtualized rendering

… and more at <a href="https://tanstack.com"><b>TanStack.com »</b></a>

<!-- USE THE FORCE LUKE -->


## Links discovered
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [GitHub discussions](https://github.com/TanStack/form/discussions)
- [Discord](https://discord.com/invite/WrRKjPJ)
- [CONTRIBUTING.md](https://github.com/tanstack/form/blob/main/CONTRIBUTING.md)
- [<img alt="NPM downloads for @tanstack/form-core" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="Star TanStack Form on GitHub" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="Minified + gzipped bundle size of @tanstack/form-core" src="https://badgen.net/bundlephobia/minzip/@tanstack/form-core" />](https://bundlephobia.com/package/@tanstack/form-core@latest)
- [<img alt="TanStack Form featured on Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%2Fform%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social" alt="Follow @TanStack"/>](https://twitter.com/tan_stack)
- [Read the docs →</b>](https://tanstack.com/form)
- [<picture> <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" /> <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" /> <img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" /> </picture>](https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS)
- [<picture> <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" /> <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" /> <img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" /> </picture>](https://www.cloudflare.com?utm_source=tanstack)
- [<b>TanStack Config</b>](https://github.com/tanstack/config)
- [<b>TanStack DB</b>](https://github.com/tanstack/db)
- [<b>TanStack DevTools</b>](https://github.com/tanstack/devtools)
- [<b>TanStack Pacer</b>](https://github.com/tanstack/pacer)
- [<b>TanStack Query</b>](https://github.com/tanstack/query)
- [<b>TanStack Ranger</b>](https://github.com/tanstack/ranger)
- [<b>TanStack Router</b>](https://github.com/tanstack/router)
- [<b>TanStack Start</b>](https://github.com/tanstack/router)
- [<b>TanStack Store</b>](https://github.com/tanstack/store)
- [<b>TanStack Table</b>](https://github.com/tanstack/table)
- [<b>TanStack Virtual</b>](https://github.com/tanstack/virtual)
- [<b>TanStack.com »</b>](https://tanstack.com)

--- eslint.config.js ---
// @ts-check

// @ts-ignore Needed due to moduleResolution Node vs Bundler
import { tanstackConfig } from '@tanstack/eslint-config'

export default [
  ...tanstackConfig,
  {
    name: 'tanstack/temp',
    rules: {
      '@typescript-eslint/array-type': 'off',
      '@typescript-eslint/method-signature-style': 'off',
      '@typescript-eslint/naming-convention': 'off',
      '@typescript-eslint/no-unnecessary-type-assertion': 'off',
      '@typescript-eslint/no-unsafe-function-type': 'off',
      '@typescript-eslint/require-await': 'off',
      'no-async-promise-executor': 'off',
      'no-empty': 'off',
    },
  },
]


--- prettier.config.js ---
// @ts-check

/** @type {import('prettier').Config} */
const config = {
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
}

export default config


--- scripts/verify-links.ts ---
import { existsSync, readFileSync, statSync } from 'node:fs'
import { extname, resolve } from 'node:path'
import { glob } from 'tinyglobby'
// @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'.
import markdownLinkExtractor from 'markdown-link-extractor'

const errors: Array<{
  file: string
  link: string
  resolvedPath: string
  reason: string
}> = []

function isRelativeLink(link: string) {
  return (
    !link.startsWith('/') &&
    !link.startsWith('http://') &&
    !link.startsWith('https://') &&
    !link.startsWith('//') &&
    !link.startsWith('#') &&
    !link.startsWith('mailto:')
  )
}

/** Remove any trailing .md */
function stripExtension(p: string): string {
  return p.replace(`${extname(p)}`, '')
}

function relativeLinkExists(link: string, file: string): boolean {
  // Remove hash if present
  const linkWithoutHash = link.split('#')[0]
  // If the link is empty after removing hash, it's not a file
  if (!linkWithoutHash) return false

  // Strip the file/link extensions
  const filePath = stripExtension(file)
  const linkPath = stripExtension(linkWithoutHash)

  // Resolve the path relative to the markdown file's directory
  // Nav up a level to simulate how links are resolved on the web
  let absPath = resolve(filePath, '..', linkPath)

  // Ensure the resolved path is within /docs
  const docsRoot = resolve('docs')
  if (!absPath.startsWith(docsRoot)) {
    errors.push({
      link,
      file,
      resolvedPath: absPath,
      reason: 'Path outside /docs',
    })
    return false
  }

  // Check if this is an example path
  const isExample = absPath.includes('/examples/')

  let exists = false

  if (isExample) {
    // Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
    absPath = absPath.replace(
      /\/docs\/framework\/([^/]+)\/examples\//,
      '/examples/$1/',
    )
    // For examples, we want to check if the directory exists
    exists = existsSync(absPath) && statSync(absPath).isDirectory()
  } else {
    // For non-examples, we want to check if the .md file exists
    if (!absPath.endsWith('.md')) {
      absPath = `${absPath}.md`
    }
    exists = existsSync(absPath)
  }

  if (!exists) {
    errors.push({
      link,
      file,
      resolvedPath: absPath,
      reason: 'Not found',
    })
  }
  return exists
}

async function verifyMarkdownLinks() {
  // Find all markdown files in docs directory
  const markdownFiles = await glob('docs/**/*.md', {
    ignore: ['**/node_modules/**'],
  })

  console.log(`Found ${markdownFiles.length} markdown files\n`)

  // Process each file
  for (const file of markdownFiles) {
    const content = readFileSync(file, 'utf-8')
    const links: Array<string> = markdownLinkExtractor(content)

    const relativeLinks = links.filter((link: string) => {
      return isRelativeLink(link)
    })

    if (relativeLinks.length > 0) {
      relativeLinks.forEach((link) => {
        relativeLinkExists(link, file)
      })
    }
  }

  if (errors.length > 0) {
    console.log(`\n❌ Found ${errors.length} broken links:`)
    errors.forEach((err) => {
      console.log(
        `${err.file}\n  link:      ${err.link}\n  resolved:  ${err.resolvedPath}\n  why:       ${err.reason}\n`,
      )
    })
    process.exit(1)
  } else {
    console.log('\n✅ No broken links found!')
  }
}

verifyMarkdownLinks().catch(console.error)


--- packages/angular-form/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Directives for managing form state in Angular

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22angular-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/angular-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/angular-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/angular-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/angular-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22angular-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/angular-form" />](https://bundlephobia.com/package/@tanstack/angular-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/form-devtools/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Devtools for the TanStack form library.

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22solid-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/solid-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/solid-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/solid-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/solid-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22solid-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/solid-form" />](https://bundlephobia.com/package/@tanstack/solid-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/lit-form/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Controllers for managing form state in Lit

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22lit-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/lit-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/lit-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/lit-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/lit-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22lit-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/lit-form" />](https://bundlephobia.com/package/@tanstack/lit-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/react-form-devtools/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Hooks for managing form state in React

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/react-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />](https://bundlephobia.com/package/@tanstack/react-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/react-form-nextjs/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Hooks for managing form state in React

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/react-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />](https://bundlephobia.com/package/@tanstack/react-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/react-form-remix/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Hooks for managing form state in React

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/react-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />](https://bundlephobia.com/package/@tanstack/react-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/react-form-start/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Hooks for managing form state in React

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/react-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />](https://bundlephobia.com/package/@tanstack/react-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/react-form/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Hooks for managing form state in React

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/react-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/react-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22react-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-form" />](https://bundlephobia.com/package/@tanstack/react-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/solid-form/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Signals for managing form state in Solid

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22solid-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/solid-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/solid-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/solid-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/solid-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22solid-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/solid-form" />](https://bundlephobia.com/package/@tanstack/solid-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)

--- packages/vue-form/README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)

Compositions for managing form state in Vue

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
  <img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
  <img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22vue-form+tests%22">
<img src="https://github.com/TanStack/form/workflows/vue-form%20tests/badge.svg" />
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
</a><a href="https://bundlephobia.com/package/@tanstack/vue-form@latest" target="\_parent">
  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/vue-form" />
</a><a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a><a href="https://github.com/TanStack/form/discussions">
  <img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
  <img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
</a> <a href="https://gitpod.io/from-referrer/">
  <img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)

## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

<!-- Use the force, Luke -->


## Links discovered
- [TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png)
- [TanStack](https://tanstack.com)
- [TanStack Table](https://github.com/TanStack/table)
- [TanStack Router](https://github.com/tanstack/router)
- [TanStack Virtual](https://github.com/tanstack/virtual)
- [React Charts](https://github.com/TanStack/react-charts)
- [React Ranger](https://github.com/TanStack/ranger)
- [tanstack.com/form](https://tanstack.com/form)
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">](https://twitter.com/intent/tweet?button_hashtag=TanStack)
- [<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />](https://discord.com/invite/WrRKjPJ)
- [<img src="https://github.com/TanStack/form/workflows/vue-form%20tests/badge.svg" />](https://github.com/TanStack/form/actions?query=workflow%3A%22vue-form+tests%22)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />](https://www.npmjs.com/package/@tanstack/form-core)
- [<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/vue-form" />](https://bundlephobia.com/package/@tanstack/vue-form@latest)
- [<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />](https://github.com/TanStack/form/discussions)
- [<img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" />](https://bestofjs.org/projects/tanstack-form)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />](https://github.com/TanStack/form/)
- [<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />](https://twitter.com/tannerlinsley)
- [<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>](https://gitpod.io/from-referrer/)
