Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set field errors from the form validators #656

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3841d84
Set field errors from the form's validators
fulopkovacs Mar 27, 2024
a453e39
Fix some failing tests
fulopkovacs Aug 11, 2024
3e5d8c1
Fix a test (comment to be added later)
fulopkovacs Aug 12, 2024
8e0e89c
Fix another test
fulopkovacs Aug 12, 2024
2f5b7b4
Shameful new line
fulopkovacs Aug 12, 2024
374bf09
Update "pnpm-lock.yaml" after the rebase again
fulopkovacs Aug 12, 2024
4f3d7d3
ci: apply automated fixes
autofix-ci[bot] Aug 12, 2024
b8750bd
Fix the sherif tests
fulopkovacs Aug 12, 2024
ea32b04
Update "pnpm-lock.yaml" after the rebase again
fulopkovacs Aug 12, 2024
8f01c06
Fix some type errors
fulopkovacs Aug 12, 2024
bc0f4eb
Fix a failing test after the rebase
fulopkovacs Aug 18, 2024
acb374e
Update the field-errors-from-form-validators examples
fulopkovacs Aug 18, 2024
7bbff3a
Reorganize the code a bit
fulopkovacs Aug 18, 2024
ec2e46d
Update the docs
fulopkovacs Aug 18, 2024
9db3846
Update pnpm-lock.yaml
fulopkovacs Aug 19, 2024
2319faa
Rebase again
fulopkovacs Aug 19, 2024
7fbd43b
ci: apply automated fixes
autofix-ci[bot] Aug 19, 2024
9615f90
Remove the doc pages that will be autogenerated
fulopkovacs Aug 19, 2024
3ef8db1
Clean up the code
fulopkovacs Aug 19, 2024
2b80c7b
Clean up the code around fake timers
fulopkovacs Aug 19, 2024
a9aee76
Clean up the tests
fulopkovacs Aug 19, 2024
81356ed
Update the FieldApi tests
fulopkovacs Aug 19, 2024
5134c02
Update the field-errors-from-form-validators example
fulopkovacs Aug 21, 2024
5c51369
Fix an example in the docs
fulopkovacs Aug 21, 2024
4845cb3
chore: fix minor issues with demo
crutchcorn Aug 27, 2024
25f41e7
Merge branch 'main' into field-errors-from-form-validators
crutchcorn Aug 27, 2024
ce10f24
docs: update docs
crutchcorn Aug 27, 2024
4cc619e
chore: fix pnpm
crutchcorn Aug 27, 2024
7f69e92
ci: apply automated fixes
autofix-ci[bot] Aug 27, 2024
2eb2ae1
chore: fix sherif
crutchcorn Aug 27, 2024
a43feb6
Merge branch 'field-errors-from-form-validators' of https://github.co…
crutchcorn Aug 27, 2024
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
103 changes: 103 additions & 0 deletions docs/framework/react/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,109 @@ export default function App() {
}
```

### Setting field-level errors from the form's validators

You can set errors on the fields from the form's validators. One common use case for this is validating all the fields on submit by calling a single API endpoint in the form's `onSubmitAsync` validator.

```tsx
export default function App() {
const form = useForm({
defaultValues: {
age: 0,
},
validators: {
onSubmitAsync: async ({ value }) => {
// Verify the age on the server
const isOlderThan13 = await verifyAgeOnServer(value.age)
if (!isOlderThan13) {
return {
form: 'Invalid data', // The `form` key is optional
fields: {
age: 'Must be 13 or older to sign',
},
}
}

return null
},
},
})

return (
<div>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
void form.handleSubmit()
}}
>
<form.Field name="age">
{(field) => (
<>
<label htmlFor={field.name}>Age:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors ? (
<em role="alert">{field.state.meta.errors.join(', ')}</em>
) : null}
</>
)}
</form.Field>
<form.Subscribe
selector={(state) => [state.errorMap]}
children={([errorMap]) =>
errorMap.onSubmit ? (
<div>
<em>There was an error on the form: {errorMap.onSubmit}</em>
</div>
) : null
}
/>
{/*...*/}
</form>
</div>
)
}
```

> Something worth mentioning is that if you have a form validation function that returns an error, that error may be overwritten by the field-specific validation.
>
> This means that:
>
> ```jsx
> const form = useForm({
> defaultValues: {
> age: 0,
> },
> validators: {
> onChange: ({ value }) => {
> return {
> fields: {
> age: value.age < 12 ? 'Too young!' : undefined,
> },
> }
> },
> },
> })
>
> // ...
>
> return <form.Field
> name="age"
> validators={{
> onChange: ({ value }) => value % 2 === 0 ? 'Must be odd!' : undefined,
> }}
> />
> ```
>
> Will only show `'Must be odd!` even if the 'Too young!' error is returned by the form-level validation.

## Asynchronous Functional Validation

While we suspect most validations will be synchronous, there are many instances where a network call or some other async operation would be useful to validate against.
Expand Down
11 changes: 11 additions & 0 deletions examples/react/field-errors-from-form-validators/.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/field-errors-from-form-validators/.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/field-errors-from-form-validators/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`
18 changes: 18 additions & 0 deletions examples/react/field-errors-from-form-validators/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!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 Field Errors From Form Validators 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>
34 changes: 34 additions & 0 deletions examples/react/field-errors-from-form-validators/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@tanstack/field-errors-from-form-validators",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"preview": "vite preview",
"test:types": "tsc"
},
"dependencies": {
"@tanstack/react-form": "^0.29.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions examples/react/field-errors-from-form-validators/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useForm } from '@tanstack/react-form'
import * as React from 'react'
import { createRoot } from 'react-dom/client'

async function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}

async function verifyAgeOnServer(age: number) {
await sleep(Math.floor(Math.random() * 1000))
return age <= 13
}

async function checkIfUsernameIsTaken(name: string) {
await sleep(Math.floor(Math.random() * 500))
const usernames = ['user-1', 'user-2', 'user-3']
return !usernames.includes(name)
}

export default function App() {
const form = useForm({
defaultValues: {
username: '',
age: 0,
},
validators: {
onSubmitAsync: async ({ value }) => {
const [isRightAge, isUsernameAvailable] = await Promise.all([
// Verify the age on the server
verifyAgeOnServer(value.age),
// Verify the availability of the username on the server
checkIfUsernameIsTaken(value.username),
])

if (!isRightAge || !isUsernameAvailable) {
return {
// The `form` key is optional
form: 'Invalid data',
fields: {
...(!isRightAge ? { age: 'Must be 13 or older to sign' } : {}),
...(!isUsernameAvailable
? { username: 'Username is taken' }
: {}),
},
}
}

return null
},
},
})

return (
<div>
<h1>Field Errors From The Form's validators Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
void form.handleSubmit()
}}
>
<form.Field
name="username"
validators={{
onSubmit: ({ value }) => (!value ? 'Required field' : null),
}}
children={(field) => (
<div>
<label htmlFor={field.name}>Username:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value)
}}
/>
{field.state.meta.errors.length > 0 ? (
<em role="alert">{field.state.meta.errors.join(', ')}</em>
) : null}
</div>
)}
/>

<form.Field
name="age"
validators={{
onSubmit: ({ value }) => (!value ? 'Required field' : null),
}}
children={(field) => (
<div>
<label htmlFor={field.name}>Age:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.length > 0 ? (
<em role="alert">{field.state.meta.errors.join(', ')}</em>
) : null}
</div>
)}
/>
<form.Subscribe
selector={(state) => [state.errorMap]}
children={([errorMap]) =>
errorMap.onSubmit ? (
<div>
<em>There was an error on the form: {errorMap.onSubmit}</em>
</div>
) : null
}
/>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
/>
</form>
</div>
)
}

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

createRoot(rootElement).render(<App />)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"jsx": "react",
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"lib": ["DOM", "DOM.Iterable", "ES2020"]
}
}
Loading