Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
9edad01
feat: initial work on FormGroupApi in core
crutchcorn Apr 16, 2026
dbc7ff6
chore: initial (and wrong) per-form-group-validation
crutchcorn Apr 16, 2026
1a22f52
chore: create a common FieldLikeAPI to adopt in form groups shortly
crutchcorn Apr 16, 2026
4775996
chore: implement FieldLike API on FormGroup
crutchcorn Apr 16, 2026
756ddf8
chore: revert changes to FormApi validation logic
crutchcorn Apr 16, 2026
c553439
chore: fix type issues with validation kind
crutchcorn Apr 16, 2026
d2eaa29
chore: minor fixes
crutchcorn Apr 16, 2026
ac22e7b
Revert "chore: revert changes to FormApi validation logic"
crutchcorn Apr 16, 2026
c2d061b
chore: filter fields to validate in formgroup
crutchcorn Apr 16, 2026
433ad6e
chore: improve store
crutchcorn Apr 16, 2026
22e49a7
chore: fix tests
crutchcorn Apr 16, 2026
db88246
chore: fix another test
crutchcorn Apr 16, 2026
4eb9b0f
chore: add submitmeta test
crutchcorn Apr 16, 2026
61d36cd
ci: apply automated fixes and generate docs
autofix-ci[bot] Apr 16, 2026
9b8bcb5
chore: add FormLike methods to FormGroup
crutchcorn Apr 20, 2026
86d9000
Merge branch 'form-group' of https://github.com/TanStack/form into fo…
crutchcorn Apr 20, 2026
d990bc6
chore: add type tests
crutchcorn Apr 22, 2026
66c5db9
chore: fix build
crutchcorn Apr 22, 2026
3c8ff5c
chore: fix type tests
crutchcorn Apr 22, 2026
4e4b272
ci: apply automated fixes and generate docs
autofix-ci[bot] Apr 22, 2026
ce63f8f
Merge branch 'main' into form-group
crutchcorn May 6, 2026
6094b9c
chore: export two missing items
crutchcorn May 6, 2026
1f96984
chore: add new interop FormGroupOptions type
crutchcorn May 6, 2026
ee121e4
chore: initial implement of useFormGroup in React
crutchcorn May 6, 2026
c46e342
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
3b756ae
chore: add formGroup to useForm
crutchcorn May 6, 2026
93ec42f
chore: add initial React implementation tests
crutchcorn May 6, 2026
da467f3
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
d1471f6
chore: add type tests for React adapter
crutchcorn May 6, 2026
e3e81b9
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
23e8066
chore: fix eslint
crutchcorn May 6, 2026
c86b424
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
4b81818
chore: add multi-step wizard
crutchcorn May 6, 2026
40b2758
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
e9ee38c
fix: handle resubmissions
crutchcorn May 6, 2026
bfa9464
fix: validate on resubmit
crutchcorn May 6, 2026
a86f781
feat: add better onDynamic handling to FormGroupApi
crutchcorn May 6, 2026
fec4e73
chore: first attempt to fix onDynamic change handling on groups
crutchcorn May 6, 2026
c3d6176
chore: attempt 2
crutchcorn May 6, 2026
973fab7
chore: attempt 3
crutchcorn May 6, 2026
7546f08
chore: attempt 4
crutchcorn May 6, 2026
c8ec12e
Revert "chore: attempt 4"
crutchcorn May 6, 2026
d499d20
chore: finalize fixing form errors
crutchcorn May 6, 2026
58ed8a2
chore: fix eslint
crutchcorn May 6, 2026
f69771b
chore: regenerate lockfile
crutchcorn May 6, 2026
7d9ca0d
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
9e78281
docs: update the wizard example
crutchcorn May 6, 2026
4837dd9
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
b189273
chore: initial work on Vue FormGroup API
crutchcorn May 7, 2026
1ee623c
ci: apply automated fixes and generate docs
autofix-ci[bot] May 7, 2026
1061988
Merge branch 'main' into form-group
crutchcorn May 8, 2026
e333776
chore: revert useContext changes
crutchcorn May 8, 2026
ad5bdd7
Merge branch 'main' into form-group
crutchcorn May 8, 2026
9371cfb
feat: add preact form group
crutchcorn May 8, 2026
d9f8097
chore: fix tests and export names
crutchcorn May 8, 2026
fa7ed5e
chore: add Preact wizard demo
crutchcorn May 8, 2026
0fbc378
feat: add FormGroup support for Solid adapter
crutchcorn May 8, 2026
caf608e
chore: add formgroup tests for Solid
crutchcorn May 8, 2026
30f20f2
chore: add type tests for Solid
crutchcorn May 8, 2026
d14c6c7
ci: apply automated fixes and generate docs
autofix-ci[bot] May 8, 2026
5458c9e
chore: add Solid group example
crutchcorn May 8, 2026
dcaebbe
ci: apply automated fixes and generate docs
autofix-ci[bot] May 8, 2026
a9c2e92
chore: fix test
crutchcorn May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/preact/multi-step-wizard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `pnpm install`
- `pnpm --filter @tanstack/form-example-preact-simple dev`
12 changes: 12 additions & 0 deletions examples/preact/multi-step-wizard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TanStack Form Preact Simple Example App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions examples/preact/multi-step-wizard/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
5 changes: 5 additions & 0 deletions examples/preact/multi-step-wizard/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { WizardPage } from './features/wizard/page'

export default function App() {
return <WizardPage />
}
26 changes: 26 additions & 0 deletions examples/preact/multi-step-wizard/src/components/text-fields.tsx
Original file line number Diff line number Diff line change
@@ -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<string>()

const errors = useStore(field.store, (state) => state.meta.errors)

return (
<div>
<label>
<div>{label}</div>
<input
value={field.state.value}
onInput={(e) => field.handleChange(e.currentTarget.value)}
onBlur={field.handleBlur}
/>
</label>
{errors.map((error: { message: string }) => (
<div key={error.message} style={{ color: 'red' }}>
{error.message}
</div>
))}
</div>
)
}
34 changes: 34 additions & 0 deletions examples/preact/multi-step-wizard/src/features/wizard/page.tsx
Original file line number Diff line number Diff line change
@@ -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 && <Step1Form form={form} step={step} setStep={setStep} />}
{step === 1 && <Step2Form form={form} step={step} setStep={setStep} />}
</>
)
}
Original file line number Diff line number Diff line change
@@ -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: '',
},
},
})
Original file line number Diff line number Diff line change
@@ -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 (
<form.FormGroup
name="step1"
validators={{
onDynamic: step1Schema,
}}
onGroupSubmit={({ value: _value }) => {
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) => (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
formGroup.handleSubmit()
}}
>
<form.AppField name="step1.name">
{(field) => <field.TextField label="Step 1 Name" />}
</form.AppField>

<form.AppForm>
<form.SubscribeButton label="Submit" />
</form.AppForm>
{/* formGroup contains errorMaps and errors, just like forms and fields */}
<pre>{JSON.stringify(formGroup.state.meta.errorMap, null, 2)}</pre>
</form>
)}
</form.FormGroup>
)
},
})
Original file line number Diff line number Diff line change
@@ -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.FormGroup
name="step2"
validators={{
onDynamic: step2Schema,
}}
onGroupSubmit={({ value: _value }) => {
form.handleSubmit()
}}
>
{(formGroup) => (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
formGroup.handleSubmit()
}}
>
<form.AppField name="step2.name">
{(field) => <field.TextField label="Step 2 Name" />}
</form.AppField>

<button onClick={() => setStep(step - 1)}>Back</button>
<form.AppForm>
<form.SubscribeButton label="Submit" />
</form.AppForm>
</form>
)}
</form.FormGroup>
)
},
})
4 changes: 4 additions & 0 deletions examples/preact/multi-step-wizard/src/hooks/form-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createFormHookContexts } from '@tanstack/preact-form'

export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
27 changes: 27 additions & 0 deletions examples/preact/multi-step-wizard/src/hooks/form.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{label}
</button>
)}
</form.Subscribe>
)
}

export const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
10 changes: 10 additions & 0 deletions examples/preact/multi-step-wizard/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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(<App />, rootElement)
9 changes: 9 additions & 0 deletions examples/preact/multi-step-wizard/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"moduleResolution": "Bundler"
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions examples/preact/multi-step-wizard/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'

export default defineConfig({
plugins: [preact()],
})
11 changes: 11 additions & 0 deletions examples/react/multi-step-wizard/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions examples/react/multi-step-wizard/.gitignore
Original file line number Diff line number Diff line change
@@ -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*
6 changes: 6 additions & 0 deletions examples/react/multi-step-wizard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install`
- `npm run dev`
16 changes: 16 additions & 0 deletions examples/react/multi-step-wizard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

<title>TanStack Form React Simple Example App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions examples/react/multi-step-wizard/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
Loading
Loading