diff --git a/docs/config.json b/docs/config.json index 6b289ada6..fc8bcda28 100644 --- a/docs/config.json +++ b/docs/config.json @@ -580,6 +580,10 @@ { "label": "Devtools", "to": "framework/react/examples/devtools" + }, + { + "label": "Multistep Form", + "to": "framework/react/examples/multistep" } ] }, diff --git a/examples/react/multistep/.eslintrc.cjs b/examples/react/multistep/.eslintrc.cjs new file mode 100644 index 000000000..35853b617 --- /dev/null +++ b/examples/react/multistep/.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/multistep/.gitignore b/examples/react/multistep/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/multistep/.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/multistep/README.md b/examples/react/multistep/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/react/multistep/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/multistep/index.html b/examples/react/multistep/index.html new file mode 100644 index 000000000..5078e2046 --- /dev/null +++ b/examples/react/multistep/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Form React Multistep Form Example App + + + +
+ + + diff --git a/examples/react/multistep/package.json b/examples/react/multistep/package.json new file mode 100644 index 000000000..d33147501 --- /dev/null +++ b/examples/react/multistep/package.json @@ -0,0 +1,37 @@ +{ + "name": "@tanstack/form-example-react-multistep", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3001", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-form": "^1.28.5", + "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.19", + "@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/multistep/public/emblem-light.svg b/examples/react/multistep/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/multistep/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/multistep/src/index.tsx b/examples/react/multistep/src/index.tsx new file mode 100644 index 000000000..a5a3c0780 --- /dev/null +++ b/examples/react/multistep/src/index.tsx @@ -0,0 +1,133 @@ +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 { useForm } from '@tanstack/react-form'; +import { z } from 'zod'; +import type { AnyFieldApi, DeepKeys } from '@tanstack/react-form'; + +const userSchema = z.object({ + firstName: z.string().min(2, 'Too short'), + email: z.string().email('Invalid email'), +}); + +type FormData = z.infer; + +function FieldUI({ field, label }: { field: AnyFieldApi; label: string }) { + return ( +
+ field.handleChange(e.target.value)} + placeholder={label} + /> + {field.state.meta.errors.length > 0 && ( + + {field.state.meta.errors + .map((e) => (typeof e === 'string' ? e : e.message)) + .join(', ')} + + )} +
+ ); +} + +export default function App() { + const [step, setStep] = React.useState(0); + + const form = useForm({ + defaultValues: { + firstName: '', + email: '', + } as FormData, + validators: { onSubmit: userSchema }, + onSubmit: async ({ value }) => console.log('Final submit:', value), + }); + + const steps = [ + { + fields: ['firstName'] as const, + component: () => ( + } + /> + ), + }, + { + fields: ['email'] as const, + component: () => ( + } + /> + ), + }, + ]; + + type FormFieldName = DeepKeys; + + const validateFieldGroup = async (fields: readonly FormFieldName[]) => { + await Promise.all(fields.map((field) => form.validateField(field, 'blur'))); + return fields.every( + (field) => (form.getFieldMeta(field)?.errors.length ?? 0) === 0 + ); + }; + + const next = async () => { + const result = await validateFieldGroup(steps[step].fields); + if (result) { + setStep((s) => s + 1); + } + }; + + return ( +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + {steps[step].component()} +
+ {step > 0 && ( + + )} + {step < steps.length - 1 ? ( + + ) : ( + + )} +
+
+ ); +} + + + +const rootElement = document.getElementById('root')! + +createRoot(rootElement).render( + + + + + , +) diff --git a/examples/react/multistep/tsconfig.json b/examples/react/multistep/tsconfig.json new file mode 100644 index 000000000..22b43163b --- /dev/null +++ b/examples/react/multistep/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"] +}