diff --git a/examples/preact/multi-step-wizard/README.md b/examples/preact/multi-step-wizard/README.md
new file mode 100644
index 000000000..dec170278
--- /dev/null
+++ b/examples/preact/multi-step-wizard/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `pnpm install`
+- `pnpm --filter @tanstack/form-example-preact-simple dev`
diff --git a/examples/preact/multi-step-wizard/index.html b/examples/preact/multi-step-wizard/index.html
new file mode 100644
index 000000000..2b9d36aed
--- /dev/null
+++ b/examples/preact/multi-step-wizard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ TanStack Form Preact Simple Example App
+
+
+
+
+
+
diff --git a/examples/preact/multi-step-wizard/package.json b/examples/preact/multi-step-wizard/package.json
new file mode 100644
index 000000000..4e4e91138
--- /dev/null
+++ b/examples/preact/multi-step-wizard/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@tanstack/form-example-preact-multi-step-wizard",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3001",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-form": "^1.29.4",
+ "preact": "^10.26.4",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "vite": "^7.2.2"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/preact/multi-step-wizard/src/App.tsx b/examples/preact/multi-step-wizard/src/App.tsx
new file mode 100644
index 000000000..22fdb05ea
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/App.tsx
@@ -0,0 +1,5 @@
+import { WizardPage } from './features/wizard/page'
+
+export default function App() {
+ return
+}
diff --git a/examples/preact/multi-step-wizard/src/components/text-fields.tsx b/examples/preact/multi-step-wizard/src/components/text-fields.tsx
new file mode 100644
index 000000000..0b0409a95
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/components/text-fields.tsx
@@ -0,0 +1,26 @@
+import { useStore } from '@tanstack/preact-form'
+import { useFieldContext } from '../hooks/form-context'
+
+export function TextField({ label }: { label: string }) {
+ const field = useFieldContext()
+
+ const errors = useStore(field.store, (state) => state.meta.errors)
+
+ return (
+
+
+ {errors.map((error: { message: string }) => (
+
+ {error.message}
+
+ ))}
+
+ )
+}
diff --git a/examples/preact/multi-step-wizard/src/features/wizard/page.tsx b/examples/preact/multi-step-wizard/src/features/wizard/page.tsx
new file mode 100644
index 000000000..8edb81b73
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/features/wizard/page.tsx
@@ -0,0 +1,34 @@
+import { revalidateLogic } from '@tanstack/preact-form'
+import { z } from 'zod'
+import { useState } from 'preact/hooks'
+import { useAppForm } from '../../hooks/form'
+import { step1Schema, step2Schema, wizardFormOpts } from './shared-form'
+import { Step2Form } from './step2-subform'
+import { Step1Form } from './step1-subform'
+
+export const WizardPage = () => {
+ const [step, setStep] = useState(0)
+ const form = useAppForm({
+ ...wizardFormOpts,
+ validationLogic: revalidateLogic(),
+ validators: {
+ // onDynamic is only used when `form.handleSubmit` is called itself.
+ // When `form.FormGroup`'s `handleSubmit` is called, it will only validate the current step's schema.
+ // This means that this schema will not be called when the user submits the form group, but instead when they submit the entire form.
+ onDynamic: z.object({
+ step1: step1Schema,
+ step2: step2Schema,
+ }),
+ },
+ onSubmit: ({ value }) => {
+ alert(`Form submitted: ${JSON.stringify(value)}`)
+ },
+ })
+
+ return (
+ <>
+ {step === 0 && }
+ {step === 1 && }
+ >
+ )
+}
diff --git a/examples/preact/multi-step-wizard/src/features/wizard/shared-form.tsx b/examples/preact/multi-step-wizard/src/features/wizard/shared-form.tsx
new file mode 100644
index 000000000..6c8923517
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/features/wizard/shared-form.tsx
@@ -0,0 +1,21 @@
+import { formOptions } from '@tanstack/preact-form'
+import z from 'zod'
+
+export const step1Schema = z.object({
+ name: z.string().min(2, 'Name must be at least 2 characters'),
+})
+
+export const step2Schema = z.object({
+ name: z.string().min(3, 'Name must be at least 3 characters'),
+})
+
+export const wizardFormOpts = formOptions({
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ name: '',
+ },
+ },
+})
diff --git a/examples/preact/multi-step-wizard/src/features/wizard/step1-subform.tsx b/examples/preact/multi-step-wizard/src/features/wizard/step1-subform.tsx
new file mode 100644
index 000000000..2eb2e04c1
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/features/wizard/step1-subform.tsx
@@ -0,0 +1,46 @@
+import { withForm } from '../../hooks/form'
+import { step1Schema, wizardFormOpts } from './shared-form'
+
+export const Step1Form = withForm({
+ ...wizardFormOpts,
+ props: {
+ step: 0,
+ setStep: (_step: number) => {},
+ },
+ render: function Render({ form, step, setStep }) {
+ return (
+ {
+ setStep(step + 1)
+ }}
+ onGroupSubmitInvalid={() => {
+ // Just like a form, you can also handle invalid submits at the group level, which is useful for multi-step wizards to prevent going to the next step if the current step is invalid
+ }}
+ >
+ {(formGroup) => (
+
+ {(field) => }
+
+
+
+
+
+ {/* formGroup contains errorMaps and errors, just like forms and fields */}
+ {JSON.stringify(formGroup.state.meta.errorMap, null, 2)}
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/preact/multi-step-wizard/src/features/wizard/step2-subform.tsx b/examples/preact/multi-step-wizard/src/features/wizard/step2-subform.tsx
new file mode 100644
index 000000000..5e95c42c9
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/features/wizard/step2-subform.tsx
@@ -0,0 +1,42 @@
+import { withForm } from '../../hooks/form'
+import { step2Schema, wizardFormOpts } from './shared-form'
+
+export const Step2Form = withForm({
+ ...wizardFormOpts,
+ props: {
+ step: 1,
+ setStep: (_step: number) => {},
+ },
+ render: function Render({ form, step, setStep }) {
+ return (
+ {
+ form.handleSubmit()
+ }}
+ >
+ {(formGroup) => (
+
+ {(field) => }
+
+
+
+
+
+
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/preact/multi-step-wizard/src/hooks/form-context.tsx b/examples/preact/multi-step-wizard/src/hooks/form-context.tsx
new file mode 100644
index 000000000..459a1ab26
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/hooks/form-context.tsx
@@ -0,0 +1,4 @@
+import { createFormHookContexts } from '@tanstack/preact-form'
+
+export const { fieldContext, useFieldContext, formContext, useFormContext } =
+ createFormHookContexts()
diff --git a/examples/preact/multi-step-wizard/src/hooks/form.tsx b/examples/preact/multi-step-wizard/src/hooks/form.tsx
new file mode 100644
index 000000000..5a5a4e75e
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/hooks/form.tsx
@@ -0,0 +1,27 @@
+import { createFormHook } from '@tanstack/preact-form'
+import { TextField } from '../components/text-fields'
+import { fieldContext, formContext, useFormContext } from './form-context'
+
+function SubscribeButton({ label }: { label: string }) {
+ const form = useFormContext()
+ return (
+ state.isSubmitting}>
+ {(isSubmitting) => (
+
+ )}
+
+ )
+}
+
+export const { useAppForm, withForm, withFieldGroup } = createFormHook({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+ fieldContext,
+ formContext,
+})
diff --git a/examples/preact/multi-step-wizard/src/index.tsx b/examples/preact/multi-step-wizard/src/index.tsx
new file mode 100644
index 000000000..f324b5c1b
--- /dev/null
+++ b/examples/preact/multi-step-wizard/src/index.tsx
@@ -0,0 +1,10 @@
+import { render } from 'preact'
+import App from './App'
+
+const rootElement = document.getElementById('root')
+
+if (!rootElement) {
+ throw new Error('Root element not found')
+}
+
+render(, rootElement)
diff --git a/examples/preact/multi-step-wizard/tsconfig.json b/examples/preact/multi-step-wizard/tsconfig.json
new file mode 100644
index 000000000..9dfa26665
--- /dev/null
+++ b/examples/preact/multi-step-wizard/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact",
+ "moduleResolution": "Bundler"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/multi-step-wizard/vite.config.ts b/examples/preact/multi-step-wizard/vite.config.ts
new file mode 100644
index 000000000..bfe110c05
--- /dev/null
+++ b/examples/preact/multi-step-wizard/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/react/multi-step-wizard/.eslintrc.cjs b/examples/react/multi-step-wizard/.eslintrc.cjs
new file mode 100644
index 000000000..35853b617
--- /dev/null
+++ b/examples/react/multi-step-wizard/.eslintrc.cjs
@@ -0,0 +1,11 @@
+// @ts-check
+
+/** @type {import('eslint').Linter.Config} */
+const config = {
+ extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
+ rules: {
+ 'react/no-children-prop': 'off',
+ },
+}
+
+module.exports = config
diff --git a/examples/react/multi-step-wizard/.gitignore b/examples/react/multi-step-wizard/.gitignore
new file mode 100644
index 000000000..4673b022e
--- /dev/null
+++ b/examples/react/multi-step-wizard/.gitignore
@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+pnpm-lock.yaml
+yarn.lock
+package-lock.json
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/examples/react/multi-step-wizard/README.md b/examples/react/multi-step-wizard/README.md
new file mode 100644
index 000000000..1cf889265
--- /dev/null
+++ b/examples/react/multi-step-wizard/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install`
+- `npm run dev`
diff --git a/examples/react/multi-step-wizard/index.html b/examples/react/multi-step-wizard/index.html
new file mode 100644
index 000000000..5d0e76cd4
--- /dev/null
+++ b/examples/react/multi-step-wizard/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+ TanStack Form React Simple Example App
+
+
+
+
+
+
+
diff --git a/examples/react/multi-step-wizard/package.json b/examples/react/multi-step-wizard/package.json
new file mode 100644
index 000000000..0d3539e26
--- /dev/null
+++ b/examples/react/multi-step-wizard/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@tanstack/form-example-react-multi-step-wizard",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3001",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/react-form": "^1.29.3",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@tanstack/react-devtools": "^0.9.7",
+ "@tanstack/react-form-devtools": "^0.2.24",
+ "@types/react": "^19.0.7",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "vite": "^7.2.2"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/react/multi-step-wizard/public/emblem-light.svg b/examples/react/multi-step-wizard/public/emblem-light.svg
new file mode 100644
index 000000000..a58e69ad5
--- /dev/null
+++ b/examples/react/multi-step-wizard/public/emblem-light.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/examples/react/multi-step-wizard/src/App.tsx b/examples/react/multi-step-wizard/src/App.tsx
new file mode 100644
index 000000000..9b047365d
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/App.tsx
@@ -0,0 +1,5 @@
+import { WizardPage } from './features/wizard/page.tsx'
+
+export default function App() {
+ return
+}
diff --git a/examples/react/multi-step-wizard/src/components/text-fields.tsx b/examples/react/multi-step-wizard/src/components/text-fields.tsx
new file mode 100644
index 000000000..de74aaad6
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/components/text-fields.tsx
@@ -0,0 +1,26 @@
+import { useStore } from '@tanstack/react-form'
+import { useFieldContext } from '../hooks/form-context.tsx'
+
+export function TextField({ label }: { label: string }) {
+ const field = useFieldContext()
+
+ const errors = useStore(field.store, (state) => state.meta.errors)
+
+ return (
+
+
+ {errors.map((error: { message: string }) => (
+
+ {error.message}
+
+ ))}
+
+ )
+}
diff --git a/examples/react/multi-step-wizard/src/features/wizard/page.tsx b/examples/react/multi-step-wizard/src/features/wizard/page.tsx
new file mode 100644
index 000000000..1bf726928
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/features/wizard/page.tsx
@@ -0,0 +1,34 @@
+import { revalidateLogic } from '@tanstack/react-form'
+import { useAppForm } from '../../hooks/form.tsx'
+import { step1Schema, step2Schema, wizardFormOpts } from './shared-form.tsx'
+import { z } from 'zod'
+import { Step2Form } from './step2-subform.tsx'
+import { useState } from 'react'
+import { Step1Form } from './step1-subform.tsx'
+
+export const WizardPage = () => {
+ const [step, setStep] = useState(0)
+ const form = useAppForm({
+ ...wizardFormOpts,
+ validationLogic: revalidateLogic(),
+ validators: {
+ // onDynamic is only used when `form.handleSubmit` is called itself.
+ // When `form.FormGroup`'s `handleSubmit` is called, it will only validate the current step's schema.
+ // This means that this schema will not be called when the user submits the form group, but instead when they submit the entire form.
+ onDynamic: z.object({
+ step1: step1Schema,
+ step2: step2Schema,
+ }),
+ },
+ onSubmit: ({ value }) => {
+ alert(`Form submitted: ${JSON.stringify(value)}`)
+ },
+ })
+
+ return (
+ <>
+ {step === 0 && }
+ {step === 1 && }
+ >
+ )
+}
diff --git a/examples/react/multi-step-wizard/src/features/wizard/shared-form.tsx b/examples/react/multi-step-wizard/src/features/wizard/shared-form.tsx
new file mode 100644
index 000000000..6f059c02b
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/features/wizard/shared-form.tsx
@@ -0,0 +1,21 @@
+import { formOptions } from '@tanstack/react-form'
+import z from 'zod'
+
+export const step1Schema = z.object({
+ name: z.string().min(2, 'Name must be at least 2 characters'),
+})
+
+export const step2Schema = z.object({
+ name: z.string().min(3, 'Name must be at least 3 characters'),
+})
+
+export const wizardFormOpts = formOptions({
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ name: '',
+ },
+ },
+})
diff --git a/examples/react/multi-step-wizard/src/features/wizard/step1-subform.tsx b/examples/react/multi-step-wizard/src/features/wizard/step1-subform.tsx
new file mode 100644
index 000000000..2eb2e04c1
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/features/wizard/step1-subform.tsx
@@ -0,0 +1,46 @@
+import { withForm } from '../../hooks/form'
+import { step1Schema, wizardFormOpts } from './shared-form'
+
+export const Step1Form = withForm({
+ ...wizardFormOpts,
+ props: {
+ step: 0,
+ setStep: (_step: number) => {},
+ },
+ render: function Render({ form, step, setStep }) {
+ return (
+ {
+ setStep(step + 1)
+ }}
+ onGroupSubmitInvalid={() => {
+ // Just like a form, you can also handle invalid submits at the group level, which is useful for multi-step wizards to prevent going to the next step if the current step is invalid
+ }}
+ >
+ {(formGroup) => (
+
+ {(field) => }
+
+
+
+
+
+ {/* formGroup contains errorMaps and errors, just like forms and fields */}
+ {JSON.stringify(formGroup.state.meta.errorMap, null, 2)}
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/react/multi-step-wizard/src/features/wizard/step2-subform.tsx b/examples/react/multi-step-wizard/src/features/wizard/step2-subform.tsx
new file mode 100644
index 000000000..5e95c42c9
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/features/wizard/step2-subform.tsx
@@ -0,0 +1,42 @@
+import { withForm } from '../../hooks/form'
+import { step2Schema, wizardFormOpts } from './shared-form'
+
+export const Step2Form = withForm({
+ ...wizardFormOpts,
+ props: {
+ step: 1,
+ setStep: (_step: number) => {},
+ },
+ render: function Render({ form, step, setStep }) {
+ return (
+ {
+ form.handleSubmit()
+ }}
+ >
+ {(formGroup) => (
+
+ {(field) => }
+
+
+
+
+
+
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/react/multi-step-wizard/src/hooks/form-context.tsx b/examples/react/multi-step-wizard/src/hooks/form-context.tsx
new file mode 100644
index 000000000..1a6b376d7
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/hooks/form-context.tsx
@@ -0,0 +1,4 @@
+import { createFormHookContexts } from '@tanstack/react-form'
+
+export const { fieldContext, useFieldContext, formContext, useFormContext } =
+ createFormHookContexts()
diff --git a/examples/react/multi-step-wizard/src/hooks/form.tsx b/examples/react/multi-step-wizard/src/hooks/form.tsx
new file mode 100644
index 000000000..4dbc203e8
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/hooks/form.tsx
@@ -0,0 +1,27 @@
+import { createFormHook } from '@tanstack/react-form'
+import { fieldContext, formContext, useFormContext } from './form-context.tsx'
+import { TextField } from '../components/text-fields.tsx'
+
+function SubscribeButton({ label }: { label: string }) {
+ const form = useFormContext()
+ return (
+ state.isSubmitting}>
+ {(isSubmitting) => (
+
+ )}
+
+ )
+}
+
+export const { useAppForm, withForm, withFieldGroup } = createFormHook({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+ fieldContext,
+ formContext,
+})
diff --git a/examples/react/multi-step-wizard/src/index.tsx b/examples/react/multi-step-wizard/src/index.tsx
new file mode 100644
index 000000000..ff73d4079
--- /dev/null
+++ b/examples/react/multi-step-wizard/src/index.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react'
+import { createRoot } from 'react-dom/client'
+
+import { TanStackDevtools } from '@tanstack/react-devtools'
+import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
+
+import App from './App.tsx'
+
+const rootElement = document.getElementById('root')!
+
+createRoot(rootElement).render(
+
+
+
+
+ ,
+)
diff --git a/examples/react/multi-step-wizard/tsconfig.json b/examples/react/multi-step-wizard/tsconfig.json
new file mode 100644
index 000000000..22b43163b
--- /dev/null
+++ b/examples/react/multi-step-wizard/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/examples/react/next-server-actions-zod/package.json b/examples/react/next-server-actions-zod/package.json
index 85e1824e1..8b8f926b0 100644
--- a/examples/react/next-server-actions-zod/package.json
+++ b/examples/react/next-server-actions-zod/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@tanstack/react-form-nextjs": "^1.29.3",
- "@tanstack/react-store": "^0.9.1",
+ "@tanstack/react-store": "^0.11.0",
"next": "16.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/examples/react/next-server-actions/package.json b/examples/react/next-server-actions/package.json
index 1e1cc1f60..5eedb71dc 100644
--- a/examples/react/next-server-actions/package.json
+++ b/examples/react/next-server-actions/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@tanstack/react-form-nextjs": "^1.29.3",
- "@tanstack/react-store": "^0.9.1",
+ "@tanstack/react-store": "^0.11.0",
"next": "16.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0"
diff --git a/examples/react/remix/package.json b/examples/react/remix/package.json
index b36b30325..3114bd584 100644
--- a/examples/react/remix/package.json
+++ b/examples/react/remix/package.json
@@ -12,7 +12,7 @@
"@remix-run/react": "^2.17.1",
"@remix-run/serve": "^2.17.1",
"@tanstack/react-form-remix": "^1.29.3",
- "@tanstack/react-store": "^0.9.1",
+ "@tanstack/react-store": "^0.11.0",
"isbot": "^5.1.30",
"react": "^19.0.0",
"react-dom": "^19.0.0"
diff --git a/examples/react/tanstack-start/package.json b/examples/react/tanstack-start/package.json
index e9c8ef9b0..87da653e8 100644
--- a/examples/react/tanstack-start/package.json
+++ b/examples/react/tanstack-start/package.json
@@ -12,7 +12,7 @@
"@tanstack/react-form-start": "^1.29.3",
"@tanstack/react-router": "^1.134.9",
"@tanstack/react-start": "^1.134.9",
- "@tanstack/react-store": "^0.9.1",
+ "@tanstack/react-store": "^0.11.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/examples/solid/multi-step-wizard/.gitignore b/examples/solid/multi-step-wizard/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/examples/solid/multi-step-wizard/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/solid/multi-step-wizard/README.md b/examples/solid/multi-step-wizard/README.md
new file mode 100644
index 000000000..99613fc0a
--- /dev/null
+++ b/examples/solid/multi-step-wizard/README.md
@@ -0,0 +1,28 @@
+## Usage
+
+```bash
+$ npm install # or pnpm install or yarn install
+```
+
+### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm run dev`
+
+Runs the app in the development mode.
+Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
+
+### `npm run build`
+
+Builds the app for production to the `dist` folder.
+It correctly bundles Solid in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.
+Your app is ready to be deployed!
+
+## Deployment
+
+Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
diff --git a/examples/solid/multi-step-wizard/index.html b/examples/solid/multi-step-wizard/index.html
new file mode 100644
index 000000000..f49678fa5
--- /dev/null
+++ b/examples/solid/multi-step-wizard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ TanStack Form Solid Simple Example App
+
+
+
+
+
+
diff --git a/examples/solid/multi-step-wizard/package.json b/examples/solid/multi-step-wizard/package.json
new file mode 100644
index 000000000..8507eab3c
--- /dev/null
+++ b/examples/solid/multi-step-wizard/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@tanstack/form-example-solid-multi-step-wizard",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "test:types": "tsc",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/solid-form": "^1.29.3",
+ "solid-js": "^1.9.9",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "typescript": "5.8.2",
+ "vite": "^7.2.2",
+ "vite-plugin-solid": "^2.11.8"
+ }
+}
diff --git a/examples/solid/multi-step-wizard/src/components/text-fields.tsx b/examples/solid/multi-step-wizard/src/components/text-fields.tsx
new file mode 100644
index 000000000..cf40ad96c
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/components/text-fields.tsx
@@ -0,0 +1,24 @@
+import { For } from 'solid-js'
+import { useStore } from '@tanstack/solid-form'
+import { useFieldContext } from '../hooks/form-context.tsx'
+
+export function TextField(props: { label: string }) {
+ const field = useFieldContext()
+
+ const errors = useStore(field().store, (state) => state.meta.errors)
+
+ return (
+
+
+
+ {(error) => {error}
}
+
+
+ )
+}
diff --git a/examples/solid/multi-step-wizard/src/features/wizard/page.tsx b/examples/solid/multi-step-wizard/src/features/wizard/page.tsx
new file mode 100644
index 000000000..312780b4e
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/features/wizard/page.tsx
@@ -0,0 +1,38 @@
+import { createSignal, Show } from 'solid-js'
+import { revalidateLogic } from '@tanstack/solid-form'
+import { z } from 'zod'
+import { useAppForm } from '../../hooks/form.tsx'
+import { step1Schema, step2Schema, wizardFormOpts } from './shared-form.tsx'
+import { Step1Form } from './step1-subform.tsx'
+import { Step2Form } from './step2-subform.tsx'
+
+export const WizardPage = () => {
+ const [step, setStep] = createSignal(0)
+ const form = useAppForm(() => ({
+ ...wizardFormOpts,
+ validationLogic: revalidateLogic(),
+ validators: {
+ // onDynamic is only used when `form.handleSubmit` is called itself.
+ // When `form.FormGroup`'s `handleSubmit` is called, it will only validate the current step's schema.
+ // This means that this schema will not be called when the user submits the form group, but instead when they submit the entire form.
+ onDynamic: z.object({
+ step1: step1Schema,
+ step2: step2Schema,
+ }),
+ },
+ onSubmit: ({ value }) => {
+ alert(`Form submitted: ${JSON.stringify(value)}`)
+ },
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/solid/multi-step-wizard/src/features/wizard/shared-form.tsx b/examples/solid/multi-step-wizard/src/features/wizard/shared-form.tsx
new file mode 100644
index 000000000..513587bd4
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/features/wizard/shared-form.tsx
@@ -0,0 +1,21 @@
+import { formOptions } from '@tanstack/solid-form'
+import z from 'zod'
+
+export const step1Schema = z.object({
+ name: z.string().min(2, 'Name must be at least 2 characters'),
+})
+
+export const step2Schema = z.object({
+ name: z.string().min(3, 'Name must be at least 3 characters'),
+})
+
+export const wizardFormOpts = formOptions({
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ name: '',
+ },
+ },
+})
diff --git a/examples/solid/multi-step-wizard/src/features/wizard/step1-subform.tsx b/examples/solid/multi-step-wizard/src/features/wizard/step1-subform.tsx
new file mode 100644
index 000000000..15bf8a3f7
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/features/wizard/step1-subform.tsx
@@ -0,0 +1,48 @@
+import { withForm } from '../../hooks/form.tsx'
+import { step1Schema, wizardFormOpts } from './shared-form.tsx'
+
+export const Step1Form = withForm({
+ ...wizardFormOpts,
+ props: {
+ step: 0,
+ setStep: (_step: number) => {},
+ },
+ render: (props) => {
+ return (
+ {
+ props.setStep(props.step + 1)
+ }}
+ onGroupSubmitInvalid={() => {
+ // Just like a form, you can also handle invalid submits at the group level, which is useful for multi-step wizards to prevent going to the next step if the current step is invalid
+ }}
+ >
+ {(formGroup) => (
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/solid/multi-step-wizard/src/features/wizard/step2-subform.tsx b/examples/solid/multi-step-wizard/src/features/wizard/step2-subform.tsx
new file mode 100644
index 000000000..6132c884c
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/features/wizard/step2-subform.tsx
@@ -0,0 +1,42 @@
+import { withForm } from '../../hooks/form.tsx'
+import { step2Schema, wizardFormOpts } from './shared-form.tsx'
+
+export const Step2Form = withForm({
+ ...wizardFormOpts,
+ props: {
+ step: 1,
+ setStep: (_step: number) => {},
+ },
+ render: (props) => {
+ return (
+ {
+ props.form.handleSubmit()
+ }}
+ >
+ {(formGroup) => (
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/solid/multi-step-wizard/src/hooks/form-context.tsx b/examples/solid/multi-step-wizard/src/hooks/form-context.tsx
new file mode 100644
index 000000000..04455937a
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/hooks/form-context.tsx
@@ -0,0 +1,4 @@
+import { createFormHookContexts } from '@tanstack/solid-form'
+
+export const { fieldContext, useFieldContext, formContext, useFormContext } =
+ createFormHookContexts()
diff --git a/examples/solid/multi-step-wizard/src/hooks/form.tsx b/examples/solid/multi-step-wizard/src/hooks/form.tsx
new file mode 100644
index 000000000..2f98ff59a
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/hooks/form.tsx
@@ -0,0 +1,25 @@
+import { createFormHook } from '@tanstack/solid-form'
+import { fieldContext, formContext, useFormContext } from './form-context.tsx'
+import { TextField } from '../components/text-fields.tsx'
+
+function SubscribeButton(props: { label: string }) {
+ const form = useFormContext()
+ return (
+ state.isSubmitting}>
+ {(isSubmitting) => (
+
+ )}
+
+ )
+}
+
+export const { useAppForm, withForm, withFieldGroup } = createFormHook({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+ fieldContext,
+ formContext,
+})
diff --git a/examples/solid/multi-step-wizard/src/index.tsx b/examples/solid/multi-step-wizard/src/index.tsx
new file mode 100644
index 000000000..88bb10c6c
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/index.tsx
@@ -0,0 +1,11 @@
+/* @refresh reload */
+import { render } from 'solid-js/web'
+import { WizardPage } from './features/wizard/page.tsx'
+
+function App() {
+ return
+}
+
+const root = document.getElementById('root')
+
+render(() => , root!)
diff --git a/examples/solid/multi-step-wizard/src/vite-env.d.ts b/examples/solid/multi-step-wizard/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/examples/solid/multi-step-wizard/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/solid/multi-step-wizard/tsconfig.json b/examples/solid/multi-step-wizard/tsconfig.json
new file mode 100644
index 000000000..576a7f010
--- /dev/null
+++ b/examples/solid/multi-step-wizard/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/solid/multi-step-wizard/vite.config.ts b/examples/solid/multi-step-wizard/vite.config.ts
new file mode 100644
index 000000000..4095d9be5
--- /dev/null
+++ b/examples/solid/multi-step-wizard/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/packages/angular-form/src/tanstack-field.ts b/packages/angular-form/src/tanstack-field.ts
index 17a3b222f..55ab5cd2f 100644
--- a/packages/angular-form/src/tanstack-field.ts
+++ b/packages/angular-form/src/tanstack-field.ts
@@ -22,8 +22,8 @@ import { injectStore } from '@tanstack/angular-store'
import type {
DeepKeys,
DeepValue,
+ FieldLikeMeta,
FieldListeners,
- FieldMeta,
FieldValidators,
FormAsyncValidateOrFn,
FormValidateOrFn,
@@ -117,7 +117,7 @@ export class TanStackField<
defaultMeta =
input<
Partial<
- FieldMeta<
+ FieldLikeMeta<
TParentData,
TName,
TData,
diff --git a/packages/form-core/package.json b/packages/form-core/package.json
index 935958a93..82b5b0097 100644
--- a/packages/form-core/package.json
+++ b/packages/form-core/package.json
@@ -53,7 +53,7 @@
"dependencies": {
"@tanstack/devtools-event-client": "^0.4.1",
"@tanstack/pacer-lite": "^0.1.1",
- "@tanstack/store": "^0.9.1"
+ "@tanstack/store": "^0.11.0"
},
"devDependencies": {
"arktype": "^2.1.22",
diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts
index 7d4a37b4c..e06f891fc 100644
--- a/packages/form-core/src/FieldApi.ts
+++ b/packages/form-core/src/FieldApi.ts
@@ -12,67 +12,32 @@ import {
mergeOpts,
} from './utils'
import { defaultValidationLogic } from './ValidationLogic'
-import type { ReadonlyStore } from '@tanstack/store'
-import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types'
-import type {
- StandardSchemaV1,
- StandardSchemaV1Issue,
- TStandardSchemaValidatorValue,
-} from './standardSchemaValidator'
+import type { AnyFormGroupApi } from './FormGroupApi'
import type {
+ FieldErrorMapFromValidator,
FieldInfo,
- FormApi,
- FormAsyncValidateOrFn,
- FormValidateAsyncFn,
- FormValidateFn,
- FormValidateOrFn,
-} from './FormApi'
-import type {
+ FieldLikeAPI,
+ FieldLikeApiOptions,
+ FieldLikeMetaBase,
+ FieldLikeOptions,
+ FieldLikeState,
ListenerCause,
+ UnwrapFieldAsyncValidateOrFn,
+ UnwrapFieldValidateOrFn,
UpdateMetaOptions,
ValidationCause,
ValidationError,
ValidationErrorMap,
- ValidationErrorMapSource,
} from './types'
+import type { ReadonlyStore } from '@tanstack/store'
+import type { DeepKeys, DeepValue } from './util-types'
+import type {
+ StandardSchemaV1,
+ TStandardSchemaValidatorValue,
+} from './standardSchemaValidator'
+import type { FormAsyncValidateOrFn, FormValidateOrFn } from './FormApi'
import type { AsyncValidator, SyncValidator, Updater } from './utils'
-/**
- * @private
- */
-// TODO: Add the `Unwrap` type to the errors
-type FieldErrorMapFromValidator<
- TFormData,
- TName extends DeepKeys,
- TData extends DeepValue,
- TOnMount extends undefined | FieldValidateOrFn,
- TOnChange extends undefined | FieldValidateOrFn,
- TOnChangeAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnBlur extends undefined | FieldValidateOrFn,
- TOnBlurAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnSubmit extends undefined | FieldValidateOrFn,
- TOnSubmitAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
-> = Partial<
- Record<
- DeepKeys,
- ValidationErrorMap<
- TOnMount,
- TOnChange,
- TOnChangeAsync,
- TOnBlur,
- TOnBlurAsync,
- TOnSubmit,
- TOnSubmitAsync
- >
- >
->
-
/**
* @private
*/
@@ -111,55 +76,6 @@ export type FieldValidateFn<
>
}) => unknown
-/**
- * @private
- */
-export type FieldValidateOrFn<
- TParentData,
- TName extends DeepKeys,
- TData extends DeepValue = DeepValue,
-> =
- | FieldValidateFn
- | StandardSchemaV1
-
-type StandardBrandedSchemaV1 = T & { __standardSchemaV1: true }
-
-type UnwrapFormValidateOrFnForInner<
- TValidateOrFn extends undefined | FormValidateOrFn,
-> = [TValidateOrFn] extends [FormValidateFn]
- ? ReturnType
- : [TValidateOrFn] extends [StandardSchemaV1]
- ? StandardBrandedSchemaV1
- : undefined
-
-export type UnwrapFieldValidateOrFn<
- TName extends string,
- TValidateOrFn extends undefined | FieldValidateOrFn,
- TFormValidateOrFn extends undefined | FormValidateOrFn,
-> =
- | ([TFormValidateOrFn] extends [StandardSchemaV1]
- ? TName extends keyof TStandardOut
- ? StandardSchemaV1Issue[]
- : undefined
- : undefined)
- | (UnwrapFormValidateOrFnForInner extends infer TFormValidateVal
- ? TFormValidateVal extends { __standardSchemaV1: true }
- ? [DeepValue] extends [never]
- ? undefined
- : StandardSchemaV1Issue[]
- : TFormValidateVal extends { fields: any }
- ? TName extends keyof TFormValidateVal['fields']
- ? TFormValidateVal['fields'][TName]
- : undefined
- : undefined
- : never)
- | ([TValidateOrFn] extends [FieldValidateFn]
- ? ReturnType
- : [TValidateOrFn] extends [StandardSchemaV1]
- ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][]
- StandardSchemaV1Issue[]
- : undefined)
-
/**
* @private
*/
@@ -199,53 +115,6 @@ export type FieldValidateAsyncFn<
signal: AbortSignal
}) => unknown | Promise
-/**
- * @private
- */
-export type FieldAsyncValidateOrFn<
- TParentData,
- TName extends DeepKeys,
- TData extends DeepValue = DeepValue,
-> =
- | FieldValidateAsyncFn
- | StandardSchemaV1
-
-type UnwrapFormAsyncValidateOrFnForInner<
- TValidateOrFn extends undefined | FormAsyncValidateOrFn,
-> = [TValidateOrFn] extends [FormValidateAsyncFn]
- ? Awaited>
- : [TValidateOrFn] extends [StandardSchemaV1]
- ? StandardBrandedSchemaV1
- : undefined
-
-export type UnwrapFieldAsyncValidateOrFn<
- TName extends string,
- TValidateOrFn extends undefined | FieldAsyncValidateOrFn,
- TFormValidateOrFn extends undefined | FormAsyncValidateOrFn,
-> =
- | ([TFormValidateOrFn] extends [StandardSchemaV1]
- ? TName extends keyof TStandardOut
- ? StandardSchemaV1Issue[]
- : undefined
- : undefined)
- | (UnwrapFormAsyncValidateOrFnForInner extends infer TFormValidateVal
- ? TFormValidateVal extends { __standardSchemaV1: true }
- ? [DeepValue] extends [never]
- ? undefined
- : StandardSchemaV1Issue[]
- : TFormValidateVal extends { fields: any }
- ? TName extends keyof TFormValidateVal['fields']
- ? TFormValidateVal['fields'][TName]
- : undefined
- : undefined
- : never)
- | ([TValidateOrFn] extends [FieldValidateAsyncFn]
- ? Awaited>
- : [TValidateOrFn] extends [StandardSchemaV1]
- ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][]
- StandardSchemaV1Issue[]
- : undefined)
-
/**
* @private
*/
@@ -284,6 +153,28 @@ export type FieldListenerFn<
>
}) => void
+/**
+ * @private
+ */
+export type FieldValidateOrFn<
+ TParentData,
+ TName extends DeepKeys,
+ TData extends DeepValue = DeepValue,
+> =
+ | FieldValidateFn
+ | StandardSchemaV1
+
+/**
+ * @private
+ */
+export type FieldAsyncValidateOrFn<
+ TParentData,
+ TName extends DeepKeys,
+ TData extends DeepValue = DeepValue,
+> =
+ | FieldValidateAsyncFn
+ | StandardSchemaV1
+
export interface FieldValidators<
TParentData,
TName extends DeepKeys,
@@ -362,514 +253,32 @@ export interface FieldValidators<
*/
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,
- TData extends DeepValue = DeepValue,
-> {
- onChange?: FieldListenerFn
- onChangeDebounceMs?: number
- onBlur?: FieldListenerFn
- onBlurDebounceMs?: number
- onMount?: FieldListenerFn
- onUnmount?: FieldListenerFn
- onSubmit?: FieldListenerFn
-}
-
-/**
- * An object type representing the options for a field in a form.
- */
-export interface FieldOptions<
- TParentData,
- TName extends DeepKeys,
- TData extends DeepValue,
- TOnMount extends undefined | FieldValidateOrFn,
- TOnChange extends undefined | FieldValidateOrFn,
- TOnChangeAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnBlur extends undefined | FieldValidateOrFn,
- TOnBlurAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnSubmit extends undefined | FieldValidateOrFn,
- TOnSubmitAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnDynamic extends undefined | FieldValidateOrFn,
- TOnDynamicAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
-> {
- /**
- * The field name. The type will be `DeepKeys` to ensure your name is a deep key of the parent dataset.
- */
- name: TName
- /**
- * An optional default value for the field.
- */
- defaultValue?: NoInfer
- /**
- * 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
- /**
- * 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,
- in out TData extends DeepValue,
- in out TOnMount extends
- | undefined
- | FieldValidateOrFn,
- in out TOnChange extends
- | undefined
- | FieldValidateOrFn,
- in out TOnChangeAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- in out TOnBlur extends
- | undefined
- | FieldValidateOrFn,
- in out TOnBlurAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- in out TOnSubmit extends
- | undefined
- | FieldValidateOrFn,
- in out TOnSubmitAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- in out TOnDynamic extends
- | undefined
- | FieldValidateOrFn,
- in out TOnDynamicAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- in out TFormOnMount extends undefined | FormValidateOrFn,
- in out TFormOnChange extends undefined | FormValidateOrFn,
- in out TFormOnChangeAsync extends
- | undefined
- | FormAsyncValidateOrFn,
- in out TFormOnBlur extends undefined | FormValidateOrFn,
- in out TFormOnBlurAsync extends
- | undefined
- | FormAsyncValidateOrFn,
- in out TFormOnSubmit extends undefined | FormValidateOrFn,
- in out TFormOnSubmitAsync extends
- | undefined
- | FormAsyncValidateOrFn,
- in out TFormOnDynamic extends undefined | FormValidateOrFn,
- in out TFormOnDynamicAsync extends
- | undefined
- | FormAsyncValidateOrFn,
- in out TFormOnServer extends undefined | FormAsyncValidateOrFn,
- in out TParentSubmitMeta,
-> extends 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,
- TData extends DeepValue,
- TOnMount extends undefined | FieldValidateOrFn,
- TOnChange extends undefined | FieldValidateOrFn,
- TOnChangeAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnBlur extends undefined | FieldValidateOrFn,
- TOnBlurAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnSubmit extends undefined | FieldValidateOrFn,
- TOnSubmitAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnDynamic extends undefined | FieldValidateOrFn,
- TOnDynamicAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TFormOnMount extends undefined | FormValidateOrFn,
- TFormOnChange extends undefined | FormValidateOrFn,
- TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnBlur extends undefined | FormValidateOrFn,
- TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnSubmit extends undefined | FormValidateOrFn,
- TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnDynamic extends undefined | FormValidateOrFn,
- TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
-> = {
- /**
- * 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,
- UnwrapFieldValidateOrFn,
- UnwrapFieldAsyncValidateOrFn,
- UnwrapFieldValidateOrFn,
- UnwrapFieldAsyncValidateOrFn,
- UnwrapFieldValidateOrFn,
- UnwrapFieldAsyncValidateOrFn,
- UnwrapFieldValidateOrFn,
- UnwrapFieldAsyncValidateOrFn
- >
-
- /**
- * @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,
- TData extends DeepValue,
- TOnMount extends undefined | FieldValidateOrFn,
- TOnChange extends undefined | FieldValidateOrFn,
- TOnChangeAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnBlur extends undefined | FieldValidateOrFn,
- TOnBlurAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnSubmit extends undefined | FieldValidateOrFn,
- TOnSubmitAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnDynamic extends undefined | FieldValidateOrFn,
- TOnDynamicAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TFormOnMount extends undefined | FormValidateOrFn,
- TFormOnChange extends undefined | FormValidateOrFn,
- TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnBlur extends undefined | FormValidateOrFn,
- TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnSubmit extends undefined | FormValidateOrFn,
- TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnDynamic extends undefined | FormValidateOrFn,
- TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
-> = {
- /**
- * An array of errors related to the field value.
- */
- errors: Array<
- | UnwrapOneLevelOfArray<
- UnwrapFieldValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldAsyncValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldAsyncValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldAsyncValidateOrFn
- >
- | UnwrapOneLevelOfArray<
- UnwrapFieldValidateOrFn
- >
- | 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,
- TData extends DeepValue,
- TOnMount extends undefined | FieldValidateOrFn,
- TOnChange extends undefined | FieldValidateOrFn,
- TOnChangeAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnBlur extends undefined | FieldValidateOrFn,
- TOnBlurAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnSubmit extends undefined | FieldValidateOrFn,
- TOnSubmitAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TOnDynamic extends undefined | FieldValidateOrFn,
- TOnDynamicAsync extends
- | undefined
- | FieldAsyncValidateOrFn,
- TFormOnMount extends undefined | FormValidateOrFn,
- TFormOnChange extends undefined | FormValidateOrFn,
- TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnBlur extends undefined | FormValidateOrFn,
- TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnSubmit extends undefined | FormValidateOrFn,
- TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnDynamic extends undefined | FormValidateOrFn,
- TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
-> = 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 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
+}
-/**
- * An object type representing the state of a field.
- */
-export type FieldState<
+export interface FieldListeners<
+ TParentData,
+ TName extends DeepKeys,
+ TData extends DeepValue = DeepValue,
+> {
+ onChange?: FieldListenerFn
+ onChangeDebounceMs?: number
+ onBlur?: FieldListenerFn
+ onBlurDebounceMs?: number
+ onMount?: FieldListenerFn
+ onUnmount?: FieldListenerFn
+ onSubmit?: FieldListenerFn
+ onGroupSubmit?: FieldListenerFn
+}
+
+interface FieldExtraOptions<
TParentData,
TName extends DeepKeys,
TData extends DeepValue,
@@ -890,24 +299,11 @@ export type FieldState<
TOnDynamicAsync extends
| undefined
| FieldAsyncValidateOrFn,
- TFormOnMount extends undefined | FormValidateOrFn,
- TFormOnChange extends undefined | FormValidateOrFn,
- TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnBlur extends undefined | FormValidateOrFn,
- TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnSubmit extends undefined | FormValidateOrFn,
- TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
- TFormOnDynamic extends undefined | FormValidateOrFn,
- TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
-> = {
- /**
- * The current value of the field.
- */
- value: TData
+> {
/**
- * The current metadata of the field.
+ * A list of validators to pass to the field
*/
- meta: FieldMeta<
+ validators?: FieldValidators<
TParentData,
TName,
TData,
@@ -919,19 +315,162 @@ export type FieldState<
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
- TOnDynamicAsync,
- TFormOnMount,
- TFormOnChange,
- TFormOnChangeAsync,
- TFormOnBlur,
- TFormOnBlurAsync,
- TFormOnSubmit,
- TFormOnSubmitAsync,
- TFormOnDynamic,
- TFormOnDynamicAsync
+ TOnDynamicAsync
>
+
+ /**
+ * A list of listeners which attach to the corresponding events
+ */
+ listeners?: FieldListeners
}
+/**
+ * An object type representing the options for a field in a form.
+ */
+export interface FieldOptions<
+ TParentData,
+ TName extends DeepKeys,
+ TData extends DeepValue,
+ TOnMount extends undefined | FieldValidateOrFn,
+ TOnChange extends undefined | FieldValidateOrFn,
+ TOnChangeAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ TOnBlur extends undefined | FieldValidateOrFn,
+ TOnBlurAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ TOnSubmit extends undefined | FieldValidateOrFn,
+ TOnSubmitAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ TOnDynamic extends undefined | FieldValidateOrFn,
+ TOnDynamicAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+>
+ extends
+ FieldExtraOptions<
+ TParentData,
+ TName,
+ TData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync
+ >,
+ FieldLikeOptions<
+ TParentData,
+ TName,
+ TData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync
+ > {}
+
+export interface FieldApiOptions<
+ in out TParentData,
+ in out TName extends DeepKeys,
+ in out TData extends DeepValue,
+ in out TOnMount extends
+ | undefined
+ | FieldValidateOrFn,
+ in out TOnChange extends
+ | undefined
+ | FieldValidateOrFn,
+ in out TOnChangeAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ in out TOnBlur extends
+ | undefined
+ | FieldValidateOrFn,
+ in out TOnBlurAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ in out TOnSubmit extends
+ | undefined
+ | FieldValidateOrFn,
+ in out TOnSubmitAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ in out TOnDynamic extends
+ | undefined
+ | FieldValidateOrFn,
+ in out TOnDynamicAsync extends
+ | undefined
+ | FieldAsyncValidateOrFn,
+ in out TFormOnMount extends undefined | FormValidateOrFn,
+ in out TFormOnChange extends undefined | FormValidateOrFn,
+ in out TFormOnChangeAsync extends
+ | undefined
+ | FormAsyncValidateOrFn,
+ in out TFormOnBlur extends undefined | FormValidateOrFn,
+ in out TFormOnBlurAsync extends
+ | undefined
+ | FormAsyncValidateOrFn,
+ in out TFormOnSubmit extends undefined | FormValidateOrFn,
+ in out TFormOnSubmitAsync extends
+ | undefined
+ | FormAsyncValidateOrFn,
+ in out TFormOnDynamic extends undefined | FormValidateOrFn,
+ in out TFormOnDynamicAsync extends
+ | undefined
+ | FormAsyncValidateOrFn,
+ in out TFormOnServer extends undefined | FormAsyncValidateOrFn,
+ in out TParentSubmitMeta,
+>
+ extends
+ FieldLikeApiOptions<
+ TParentData,
+ TName,
+ TData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TFormOnMount,
+ TFormOnChange,
+ TFormOnChangeAsync,
+ TFormOnBlur,
+ TFormOnBlurAsync,
+ TFormOnSubmit,
+ TFormOnSubmitAsync,
+ TFormOnDynamic,
+ TFormOnDynamicAsync,
+ TFormOnServer,
+ TParentSubmitMeta
+ >,
+ FieldExtraOptions<
+ TParentData,
+ TName,
+ TData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync
+ > {}
+
/**
* @public
*
@@ -1027,6 +566,44 @@ export class FieldApi<
| FormAsyncValidateOrFn,
in out TFormOnServer extends undefined | FormAsyncValidateOrFn,
in out TParentSubmitMeta,
+> implements FieldLikeAPI<
+ TParentData,
+ TName,
+ TData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TFormOnMount,
+ TFormOnChange,
+ TFormOnChangeAsync,
+ TFormOnBlur,
+ TFormOnBlurAsync,
+ TFormOnSubmit,
+ TFormOnSubmitAsync,
+ TFormOnDynamic,
+ TFormOnDynamicAsync,
+ TFormOnServer,
+ TParentSubmitMeta,
+ FieldExtraOptions<
+ TParentData,
+ TName,
+ TData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync
+ >
> {
/**
* A reference to the form API instance.
@@ -1092,7 +669,7 @@ export class FieldApi<
* The field state store.
*/
store!: ReadonlyStore<
- FieldState<
+ FieldLikeState<
TParentData,
TName,
TData,
@@ -1171,7 +748,7 @@ export class FieldApi<
this.store = createStore(
(
prevVal:
- | FieldState<
+ | FieldLikeState<
TParentData,
TName,
TData,
@@ -1221,7 +798,7 @@ export class FieldApi<
return {
value,
meta,
- } as FieldState<
+ } as FieldLikeState<
TParentData,
TName,
TData,
@@ -1490,7 +1067,7 @@ export class FieldApi<
*/
setMeta = (
updater: Updater<
- FieldMetaBase<
+ FieldLikeMetaBase<
TParentData,
TName,
TData,
@@ -1653,6 +1230,10 @@ export class FieldApi<
const linkedFields: AnyFieldApi[] = []
for (const field of fields) {
if (!field.instance) continue
+ // TODO: How to handle FieldGroups? Do we need to? IDK.
+ if (!(field.instance instanceof FieldApi)) {
+ continue
+ }
const { onChangeListenTo, onBlurListenTo } =
field.instance.options.validators || {}
if (cause === 'change' && onChangeListenTo?.includes(this.name)) {
@@ -1994,32 +1575,115 @@ export class FieldApi<
*/
validate = (
cause: ValidationCause,
- opts?: { skipFormValidation?: boolean },
+ opts?: { skipFormValidation?: boolean; skipGroupValidation?: boolean },
): ValidationError[] | Promise => {
// If the field is pristine, do not validate
if (!this.state.meta.isTouched) return []
+ // Cascade into any encompassing `FormGroupApi`'s own validators so
+ // group-scoped strategies (e.g. `revalidateLogic` gated on the group's
+ // own `submissionAttempts`) get a chance to react to this field change.
+ // Mirror the field's sync→short-circuit-on-error→async semantics for
+ // each group: only kick off async validators if the group's sync pass
+ // was clean (or its `asyncAlways` flag is set).
+ const encompassingGroups = opts?.skipGroupValidation
+ ? []
+ : Array.from(this.form.formGroupApis).filter((group) =>
+ this.name.startsWith(group.name),
+ )
+
// Attempt to sync validate first
- const { fieldsErrorMap } = opts?.skipFormValidation
+ const formSyncResult = opts?.skipFormValidation
? { fieldsErrorMap: {} as never }
: this.form.validateSync(cause)
- const { hasErrored } = this.validateSync(
- cause,
- fieldsErrorMap[this.name] ?? {},
- )
+ let fieldsErrorMap = (formSyncResult.fieldsErrorMap[this.name] ??
+ {}) as ValidationErrorMap
+
+ // For each encompassing group whose own submission has been attempted,
+ // also re-run the parent form's validators with that group as the
+ // gating context. This ensures form-level errors (e.g. those produced
+ // by a form-level z.object onDynamic during a group submit) are kept
+ // fresh on subsequent field changes — even though the form itself
+ // hasn't been submitted directly.
+ if (!opts?.skipFormValidation) {
+ for (const group of encompassingGroups) {
+ if (group.formState.submissionAttempts === 0) continue
+ const { fieldsErrorMap: groupFormErrors } = this.form.validateSync(
+ cause,
+ {
+ group,
+ dontUpdateFormErrorMap: true,
+ filterFieldNames: (fieldName) => fieldName.startsWith(group.name),
+ },
+ )
+ fieldsErrorMap = {
+ ...fieldsErrorMap,
+ ...(groupFormErrors[this.name] ?? {}),
+ }
+ }
+ }
+
+ const { hasErrored } = this.validateSync(cause, fieldsErrorMap)
+
+ const groupHasErroredWeakMap = new WeakMap()
+ for (const group of encompassingGroups) {
+ const { hasErrored: groupHasErrored } = group.validateSync(
+ cause,
+ {},
+ { skipRelatedFieldValidation: true },
+ )
+
+ groupHasErroredWeakMap.set(group, groupHasErrored)
+ }
if (hasErrored && !this.options.asyncAlways) {
this.getInfo().validationMetaMap[
getErrorMapKey(cause)
]?.lastAbortController.abort()
- return this.state.meta.errors
+
+ const groupErrors = [] as ValidationError[][]
+
+ for (const group of encompassingGroups) {
+ group
+ .getInfo()
+ .validationMetaMap[getErrorMapKey(cause)]?.lastAbortController.abort()
+
+ groupErrors.push(group.state.meta.errors)
+ }
+
+ return [...this.state.meta.errors, ...groupErrors.flat()]
}
// No error? Attempt async validation
const formValidationResultPromise = opts?.skipFormValidation
? Promise.resolve({})
: this.form.validateAsync(cause)
- return this.validateAsync(cause, formValidationResultPromise)
+
+ const fieldAsyncResults = this.validateAsync(
+ cause,
+ formValidationResultPromise,
+ )
+
+ const groupAsyncResults: Promise[] = []
+ for (const group of encompassingGroups) {
+ if (groupHasErroredWeakMap.get(group) && !group.options.asyncAlways) {
+ continue
+ }
+
+ groupAsyncResults.push(
+ group.validateAsync(cause, formValidationResultPromise, {
+ skipRelatedFieldValidation: true,
+ }),
+ )
+ }
+
+ if (groupAsyncResults.length === 0) {
+ return fieldAsyncResults
+ }
+
+ return Promise.all([fieldAsyncResults, ...groupAsyncResults]).then(
+ (results) => results.flat(),
+ )
}
/**
@@ -2094,7 +1758,7 @@ export class FieldApi<
)
}
- private triggerOnBlurListener() {
+ private triggerOnBlurListener = () => {
const formDebounceMs = this.form.options.listeners?.onBlurDebounceMs
if (formDebounceMs && formDebounceMs > 0) {
if (this.timeoutIds.formListeners.blur) {
@@ -2176,6 +1840,16 @@ export class FieldApi<
})
}
}
+
+ /**
+ * @private
+ */
+ triggerOnSubmitListener = () => {
+ this.options.listeners?.onSubmit?.({
+ value: this.state.value,
+ fieldApi: this,
+ })
+ }
}
function normalizeError(rawError?: ValidationError) {
diff --git a/packages/form-core/src/FieldGroupApi.ts b/packages/form-core/src/FieldGroupApi.ts
index 6cd9d0976..a7fac9951 100644
--- a/packages/form-core/src/FieldGroupApi.ts
+++ b/packages/form-core/src/FieldGroupApi.ts
@@ -1,5 +1,11 @@
import { createStore } from '@tanstack/store'
import { concatenatePaths, getBy, makePathArray } from './utils'
+import type {
+ AnyFieldLikeMetaBase,
+ FormLikeAPI,
+ UpdateMetaOptions,
+ ValidationCause,
+} from './types'
import type { ReadonlyStore } from '@tanstack/store'
import type { Updater } from './utils'
import type {
@@ -7,18 +13,13 @@ import type {
FormAsyncValidateOrFn,
FormValidateOrFn,
} from './FormApi'
-import type { AnyFieldMetaBase, FieldOptions } from './FieldApi'
+import type { FieldOptions } from './FieldApi'
import type {
DeepKeys,
DeepKeysOfType,
DeepValue,
FieldsMap,
} from './util-types'
-import type {
- FieldManipulator,
- UpdateMetaOptions,
- ValidationCause,
-} from './types'
export type AnyFieldGroupApi = FieldGroupApi<
any,
@@ -127,7 +128,7 @@ export class FieldGroupApi<
in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
in out TOnServer extends undefined | FormAsyncValidateOrFn,
in out TSubmitMeta = never,
-> implements FieldManipulator {
+> implements FormLikeAPI {
/**
* The form that called this field group.
*/
@@ -368,7 +369,7 @@ export class FieldGroupApi<
*/
setFieldMeta = >(
field: TField,
- updater: Updater,
+ updater: Updater,
) => {
return this.form.setFieldMeta(this.getFormFieldName(field), updater)
}
diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts
index 2317fb2b9..e6cecb443 100644
--- a/packages/form-core/src/FormApi.ts
+++ b/packages/form-core/src/FormApi.ts
@@ -21,24 +21,12 @@ import {
} from './standardSchemaValidator'
import { defaultFieldMeta, metaHelper } from './metaHelper'
import { formEventClient } from './EventClient'
-import type { ReadonlyStore, Store } from '@tanstack/store'
-
-// types
-import type { ValidationLogicFn } from './ValidationLogic'
-import type {
- StandardSchemaV1,
- StandardSchemaV1Issue,
- TStandardSchemaValidatorValue,
-} from './standardSchemaValidator'
-import type {
- AnyFieldApi,
- AnyFieldMeta,
- AnyFieldMetaBase,
- FieldApi,
-} from './FieldApi'
import type {
+ AnyFieldLikeMeta,
+ AnyFieldLikeMetaBase,
ExtractGlobalFormError,
- FieldManipulator,
+ FieldInfo,
+ FormLikeAPI,
FormValidationError,
FormValidationErrorMap,
GlobalFormValidationError,
@@ -49,6 +37,17 @@ import type {
ValidationErrorMap,
ValidationErrorMapKeys,
} from './types'
+import type { ReadonlyStore, Store } from '@tanstack/store'
+
+// types
+import type { ValidationLogicFn } from './ValidationLogic'
+import type {
+ StandardSchemaV1,
+ StandardSchemaV1Issue,
+ TStandardSchemaValidatorValue,
+} from './standardSchemaValidator'
+import type { AnyFieldApi } from './FieldApi'
+import type { AnyFormGroupApi } from './FormGroupApi'
import type { DeepKeys, DeepKeysOfType, DeepValue } from './util-types'
import type { Updater } from './utils'
@@ -503,44 +502,6 @@ export type ValidationMeta = {
lastAbortController: AbortController
}
-/**
- * An object representing the field information for a specific field within the form.
- */
-export type FieldInfo = {
- /**
- * 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
-}
-
/**
* An object representing the current state of the form.
*/
@@ -583,7 +544,7 @@ export type BaseFormState<
/**
* A record of field metadata for each field in the form, not including the derived properties, like `errors` and such
*/
- fieldMetaBase: Partial, AnyFieldMetaBase>>
+ fieldMetaBase: Partial, AnyFieldLikeMetaBase>>
/**
* A boolean indicating if the form is currently in the process of being submitted after `handleSubmit` is called.
*
@@ -712,7 +673,7 @@ export type DerivedFormState<
/**
* A record of field metadata for each field in the form.
*/
- fieldMeta: Partial, AnyFieldMeta>>
+ fieldMeta: Partial