Skip to content

Commit 0ae60dc

Browse files
committed
docs: Added multistep form example to examples/react
1 parent e21cc01 commit 0ae60dc

9 files changed

Lines changed: 270 additions & 0 deletions

File tree

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,10 @@
580580
{
581581
"label": "Devtools",
582582
"to": "framework/react/examples/devtools"
583+
},
584+
{
585+
"label": "Multistep Form",
586+
"to": "framework/react/examples/multistep"
583587
}
584588
]
585589
},
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @ts-check
2+
3+
/** @type {import('eslint').Linter.Config} */
4+
const config = {
5+
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
6+
rules: {
7+
'react/no-children-prop': 'off',
8+
},
9+
}
10+
11+
module.exports = config
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
pnpm-lock.yaml
15+
yarn.lock
16+
package-lock.json
17+
18+
# misc
19+
.DS_Store
20+
.env.local
21+
.env.development.local
22+
.env.test.local
23+
.env.production.local
24+
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*

examples/react/multistep/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
9+
<title>TanStack Form React Multistep Form Example App</title>
10+
</head>
11+
<body>
12+
<noscript>You need to enable JavaScript to run this app.</noscript>
13+
<div id="root"></div>
14+
<script type="module" src="/src/index.tsx"></script>
15+
</body>
16+
</html>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@tanstack/form-example-react-multistep",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port=3001",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "tsc"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-form": "^1.28.5",
13+
"react": "^19.0.0",
14+
"react-dom": "^19.0.0",
15+
"zod": "^3.25.76"
16+
},
17+
"devDependencies": {
18+
"@tanstack/react-devtools": "^0.9.7",
19+
"@tanstack/react-form-devtools": "^0.2.19",
20+
"@types/react": "^19.0.7",
21+
"@types/react-dom": "^19.0.3",
22+
"@vitejs/plugin-react": "^5.1.1",
23+
"vite": "^7.2.2"
24+
},
25+
"browserslist": {
26+
"production": [
27+
">0.2%",
28+
"not dead",
29+
"not op_mini all"
30+
],
31+
"development": [
32+
"last 1 chrome version",
33+
"last 1 firefox version",
34+
"last 1 safari version"
35+
]
36+
}
37+
}
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import * as React from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import { TanStackDevtools } from '@tanstack/react-devtools'
4+
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
5+
import { useForm } from '@tanstack/react-form';
6+
import { z } from 'zod';
7+
import type { AnyFieldApi, DeepKeys } from '@tanstack/react-form';
8+
9+
const userSchema = z.object({
10+
firstName: z.string().min(2, 'Too short'),
11+
email: z.string().email('Invalid email'),
12+
});
13+
14+
type FormData = z.infer<typeof userSchema>;
15+
16+
function FieldUI({ field, label }: { field: AnyFieldApi; label: string }) {
17+
return (
18+
<div>
19+
<input
20+
key={label}
21+
value={field.state.value}
22+
onBlur={field.handleBlur}
23+
onChange={(e) => field.handleChange(e.target.value)}
24+
placeholder={label}
25+
/>
26+
{field.state.meta.errors.length > 0 && (
27+
<em style={{ color: 'red' }}>
28+
{field.state.meta.errors
29+
.map((e) => (typeof e === 'string' ? e : e.message))
30+
.join(', ')}
31+
</em>
32+
)}
33+
</div>
34+
);
35+
}
36+
37+
export default function App() {
38+
const [step, setStep] = React.useState(0);
39+
40+
const form = useForm({
41+
defaultValues: {
42+
firstName: '',
43+
email: '',
44+
} as FormData,
45+
validators: { onSubmit: userSchema },
46+
onSubmit: async ({ value }) => console.log('Final submit:', value),
47+
});
48+
49+
const steps = [
50+
{
51+
fields: ['firstName'] as const,
52+
component: () => (
53+
<form.Field
54+
name="firstName"
55+
validators={{ onBlur: userSchema.shape.firstName }}
56+
children={(f) => <FieldUI field={f} label="First Name" />}
57+
/>
58+
),
59+
},
60+
{
61+
fields: ['email'] as const,
62+
component: () => (
63+
<form.Field
64+
name="email"
65+
validators={{ onBlur: userSchema.shape.email }}
66+
children={(f) => <FieldUI field={f} label="Email" />}
67+
/>
68+
),
69+
},
70+
];
71+
72+
type FormFieldName = DeepKeys<FormData>;
73+
74+
const validateFieldGroup = async (fields: readonly FormFieldName[]) => {
75+
await Promise.all(fields.map((field) => form.validateField(field, 'blur')));
76+
return fields.every(
77+
(field) => (form.getFieldMeta(field)?.errors.length ?? 0) === 0
78+
);
79+
};
80+
81+
const next = async () => {
82+
const result = await validateFieldGroup(steps[step].fields);
83+
if (result) {
84+
setStep((s) => s + 1);
85+
}
86+
};
87+
88+
return (
89+
<form
90+
onSubmit={(e) => {
91+
e.preventDefault();
92+
form.handleSubmit();
93+
}}
94+
>
95+
{steps[step].component()}
96+
<div style={{ marginTop: 20 }}>
97+
{step > 0 && (
98+
<button
99+
key="back"
100+
type="button"
101+
onClick={() => setStep((s) => s - 1)}
102+
>
103+
Back
104+
</button>
105+
)}
106+
{step < steps.length - 1 ? (
107+
<button key="next" type="button" onClick={next}>
108+
Next
109+
</button>
110+
) : (
111+
<button style={{ marginLeft: 20 }} type="submit">
112+
Submit
113+
</button>
114+
)}
115+
</div>
116+
</form>
117+
);
118+
}
119+
120+
121+
122+
const rootElement = document.getElementById('root')!
123+
124+
createRoot(rootElement).render(
125+
<React.StrictMode>
126+
<App />
127+
128+
<TanStackDevtools
129+
config={{ hideUntilHover: true }}
130+
plugins={[formDevtoolsPlugin()]}
131+
/>
132+
</React.StrictMode>,
133+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
5+
"module": "ESNext",
6+
"skipLibCheck": true,
7+
8+
/* Bundler mode */
9+
"moduleResolution": "Bundler",
10+
"allowImportingTsExtensions": true,
11+
"resolveJsonModule": true,
12+
"isolatedModules": true,
13+
"noEmit": true,
14+
"jsx": "react-jsx",
15+
16+
/* Linting */
17+
"strict": true,
18+
"noUnusedLocals": true,
19+
"noUnusedParameters": true,
20+
"noFallthroughCasesInSwitch": true
21+
},
22+
"include": ["src"]
23+
}

0 commit comments

Comments
 (0)