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

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
57b1c4f
Add a test for the new validator API
fulopkovacs Mar 27, 2024
6874dc5
Set field errors from the validators synchronously
fulopkovacs Mar 30, 2024
d447765
Set field errors from the validators asynchronously
fulopkovacs Mar 31, 2024
7c4c823
Add tests for React
fulopkovacs Mar 31, 2024
820e0d7
Remove some comments
fulopkovacs Mar 31, 2024
93adb4d
Prefix generics with "T"
fulopkovacs Apr 1, 2024
acbbe3e
Make the capitalization of the form error messages in the tests consi…
fulopkovacs Apr 13, 2024
8346cdc
Add tests for array field errors
fulopkovacs Apr 13, 2024
852f672
Add tests for subfields in array fields
fulopkovacs Apr 13, 2024
bf3a62d
Update the tests
fulopkovacs Apr 14, 2024
737d62f
Check if this new test runs successfully in the CI
fulopkovacs Apr 14, 2024
eb281e0
Test another change
fulopkovacs Apr 14, 2024
876a4cd
Pass down the field errors from the form to the field
fulopkovacs Apr 15, 2024
1c57e5a
Finish implementing my second approach for the sync validators
fulopkovacs Apr 15, 2024
89155a8
Async validators wip
fulopkovacs Apr 19, 2024
3c928aa
Async validators wip-2
fulopkovacs Apr 19, 2024
2511136
Add a new test (linked fields, async validation)
fulopkovacs Apr 20, 2024
b1e30f1
Add a test for React
fulopkovacs Apr 20, 2024
acc29b4
Remove a console.log
fulopkovacs Apr 20, 2024
6473b44
Remove a duplicated test
fulopkovacs Apr 21, 2024
f669cdb
Add another test
fulopkovacs Apr 21, 2024
a14caba
Remove some console.log-s
fulopkovacs Apr 21, 2024
9b33305
Revert "Remove some console.log-s"
fulopkovacs Apr 21, 2024
1c83751
Remove some console.log-s
fulopkovacs Apr 21, 2024
7009619
Remove two other console.log-s
fulopkovacs Apr 21, 2024
490e96a
Remove another console.log
fulopkovacs Apr 21, 2024
5c68771
Revert "Remove another console.log"
fulopkovacs Apr 21, 2024
222eb02
Remove three console.log-s
fulopkovacs Apr 21, 2024
624f738
Why is this console.log needed?
fulopkovacs Apr 21, 2024
a7557df
Now I remove it and have an error in the CI
fulopkovacs Apr 21, 2024
f8432a4
Rename a variable for clarity
fulopkovacs Apr 24, 2024
d943985
Update the docs
fulopkovacs Apr 27, 2024
291a1ed
Update the docs
fulopkovacs Apr 27, 2024
a425a90
Add a new example for setting field-errors from the form's validators
fulopkovacs Apr 28, 2024
3740379
Remove some redundant lines
fulopkovacs Apr 28, 2024
88c0b1e
Tidy up the internals a bit
fulopkovacs Apr 28, 2024
d386002
Fix the type of the code blocks
fulopkovacs Apr 29, 2024
23e766b
Update the docs
fulopkovacs Apr 29, 2024
a90bd11
Remove some redundant lines
fulopkovacs Apr 30, 2024
43b29aa
Remove some more redundant lines
fulopkovacs Apr 30, 2024
0d9f7f7
Small changes
fulopkovacs Apr 30, 2024
1c08b13
Small changes
fulopkovacs May 4, 2024
148ba69
Clean up the tests
fulopkovacs May 4, 2024
5bfe90b
Clean up the test 2.
fulopkovacs May 4, 2024
dbeb7d6
Fix an error
fulopkovacs May 4, 2024
da08fd9
Update the pnpm-lock.yaml file
fulopkovacs May 4, 2024
d24a4e4
Remove some lines
fulopkovacs May 5, 2024
bba46fc
Remove one more line
fulopkovacs May 5, 2024
32052d3
Remove some comments
fulopkovacs May 5, 2024
a8dbbdc
Clean up the code around the ValdiationPromiseResult type
fulopkovacs May 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/framework/react/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,76 @@ 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 }) => {
console.log({ 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.state.errorMap.onSubmit ? (
<div>
<em>
There was an error on the form: {form.state.errorMap.onSubmit}
</em>
</div>
) : null}
{/*...*/}
</form>
</div>
)
}

```

## 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
30 changes: 23 additions & 7 deletions docs/reference/formApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,34 @@ An object representing the options for a form.

### `FormValidators<TFormData, TFormValidator>`

> `FormValidationError<TFormData>` is either a `ValidationError` or an object with the following keys:
> - `form` (optional): A form error
> - `fields`: Field-level errors that should be set in the form validator
>
> Here's an example:
>
> ```tsx
> {
> form: 'This form contains an error',
> fields: {
> age: "Must be 13 or older to register"
> }
> }
> ```


- ```tsx
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
onMount?: (values: TData, formApi: FormApi<TData>) => FormValidationError<TFormData>
```
- Optional function that fires as soon as the component mounts.

- ```tsx
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError
onChange?: (values: TData, formApi: FormApi<TData>) => FormValidationError<TFormData>
```
- Optional function that checks the validity of your data whenever a value changes

- ```tsx
onChangeAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>
onChangeAsync?: (values: TData, formApi: FormApi<TData>) => FormValidationError<TFormData> | Promise<FormValidationError<TFormData>>
```
- Optional onChange asynchronous counterpart to onChange. Useful for more complex validation logic that might involve server requests.

Expand All @@ -78,14 +94,14 @@ An object representing the options for a form.
- The default time in milliseconds that if set to a number larger than 0, will debounce the async validation event by this length of time in milliseconds.

- ```tsx
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError
onBlur?: (values: TData, formApi: FormApi<TData>) => FormValidationError<TFormData>
```
- Optional function that validates the form data when a field loses focus, returns a `ValidationError`
- Optional function that validates the form data when a field loses focus, returns a `FormValidationError<TFormData>`

- ```tsx
onBlurAsync?: (values: TData,formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>
onBlurAsync?: (values: TData,formApi: FormApi<TData>) => FormValidationError<TFormData> | Promise<FormValidationError<TFormData>>
```
- Optional onBlur asynchronous validation method for when a field loses focus return a `ValidationError` or a promise of `Promise<ValidationError>`
- Optional onBlur asynchronous validation method for when a field loses focus return a `FormValidationError<TFormData>` or a promise of `Promise<FormValidationError<TFormData>>`

- ```tsx
onBlurAsyncDebounceMs?: number
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`
16 changes: 16 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,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>
32 changes: 32 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,32 @@
{
"name": "@tanstack/field-errors-from-form-validators",
"version": "0.0.1",
"main": "src/index.jsx",
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"preview": "vite preview",
"test:types": "tsc"
},
"dependencies": {
"@tanstack/react-form": "^0.19.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.10"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 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,94 @@
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { useForm } from '@tanstack/react-form'
import type { FieldApi } from '@tanstack/react-form'

function FieldInfo({ field }: { field: FieldApi<any, any, any, any> }) {
return (
<>
{field.state.meta.touchedErrors ? (
<em>{field.state.meta.touchedErrors}</em>
) : null}
{field.state.meta.isValidating ? 'Validating...' : null}
</>
)
}

async function verifyAgeOnServer(age: number) {
return age >= 13
}

export default function App() {
const form = useForm({
defaultValues: {
age: 0,
},
validators: {
onSubmitAsync: async ({ value }) => {
console.log({ 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>
<h1>Field Errors From The Form's validators Example</h1>
<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.state.errorMap.onSubmit ? (
<div>
<em>
There was an error on the form: {form.state.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"]
}
}