Skip to content

Commit 3f216c2

Browse files
authored
Merge pull request #5623 from marmelab/support-async-validators
Add better support for async validators
2 parents 57da393 + 1b63dac commit 3f216c2

File tree

6 files changed

+192
-33
lines changed

6 files changed

+192
-33
lines changed

docs/CreateEdit.md

+65
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,38 @@ export const UserCreate = (props) => (
10921092

10931093
**Tip**: The props you pass to `<SimpleForm>` and `<TabbedForm>` are passed to the [<Form>](https://final-form.org/docs/react-final-form/api/Form) of `react-final-form`.
10941094

1095+
**Tip**: The `validate` function can return a promise for asynchronous validation. For instance:
1096+
1097+
```jsx
1098+
const validateUserCreation = async (values) => {
1099+
const errors = {};
1100+
if (!values.firstName) {
1101+
errors.firstName = ['The firstName is required'];
1102+
}
1103+
if (!values.age) {
1104+
errors.age = ['The age is required'];
1105+
} else if (values.age < 18) {
1106+
errors.age = ['Must be over 18'];
1107+
}
1108+
1109+
const isEmailUnique = await checkEmailIsUnique(values.userName);
1110+
if (!isEmailUnique) {
1111+
errors.email = ['Email already used'];
1112+
}
1113+
return errors
1114+
};
1115+
1116+
export const UserCreate = (props) => (
1117+
<Create {...props}>
1118+
<SimpleForm validate={validateUserCreation}>
1119+
<TextInput label="First Name" source="firstName" />
1120+
<TextInput label="Email" source="email" />
1121+
<TextInput label="Age" source="age" />
1122+
</SimpleForm>
1123+
</Create>
1124+
);
1125+
```
1126+
10951127
### Per Input Validation: Built-in Field Validators
10961128

10971129
Alternatively, you can specify a `validate` prop directly in `<Input>` components, taking either a function or an array of functions. React-admin already bundles a few validator functions, that you can just require, and use as input-level validators:
@@ -1257,6 +1289,39 @@ export const ProductEdit = ({ ...props }) => (
12571289

12581290
**Tip**: You can use *both* Form validation and input validation.
12591291

1292+
**Tip**: The custom validator function can return a promise for asynchronous validation. For instance:
1293+
1294+
```jsx
1295+
const validateEmailUnicity = async (value) => {
1296+
const isEmailUnique = await checkEmailIsUnique(value);
1297+
if (!isEmailUnique) {
1298+
return 'Email already used';
1299+
1300+
// You can return a translation key as well
1301+
return 'myroot.validation.email_already_used';
1302+
1303+
// Or even an object just like the other validators
1304+
return { message: 'myroot.validation.email_already_used', args: { email: value } }
1305+
1306+
}
1307+
1308+
return errors
1309+
};
1310+
1311+
const emailValidators = [required(), validateEmailUnicity];
1312+
1313+
export const UserCreate = (props) => (
1314+
<Create {...props}>
1315+
<SimpleForm validate={validateUserCreation}>
1316+
...
1317+
<TextInput label="Email" source="email" validate={emailValidators} />
1318+
...
1319+
</SimpleForm>
1320+
</Create>
1321+
);
1322+
```
1323+
1324+
**Important**: Note that asynchronous validators are not supported on the `ArrayInput` component due to a limitation of [react-final-form-arrays](https://github.com/final-form/react-final-form-arrays).
12601325
## Submit On Enter
12611326

12621327
By default, pressing `ENTER` in any of the form fields submits the form - this is the expected behavior in most cases. However, some of your custom input components (e.g. Google Maps widget) may have special handlers for the `ENTER` key. In that case, to disable the automated form submission on enter, set the `submitOnEnter` prop of the form component to `false`:

docs/Inputs.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,9 @@ import { ArrayInput, SimpleFormIterator, DateInput, TextInput, FormDataConsumer
10081008
</ArrayInput>
10091009
```
10101010

1011-
`<ArrayInput>` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`).
1011+
`<ArrayInput>` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`).
1012+
1013+
**Important**: Note that asynchronous validators are not supported on the `ArrayInput` component due to a limitation of [react-final-form-arrays](https://github.com/final-form/react-final-form-arrays).
10121014

10131015
### `<AutocompleteArrayInput>`
10141016

examples/simple/src/users/UserCreate.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ const UserEditToolbar = ({
3838
</Toolbar>
3939
);
4040

41+
const isValidName = async value =>
42+
new Promise(resolve =>
43+
setTimeout(resolve(value === 'Admin' ? "Can't be Admin" : undefined))
44+
);
45+
4146
const UserCreate = ({ permissions, ...props }) => (
4247
<Create {...props} aside={<Aside />}>
4348
<TabbedForm
@@ -48,7 +53,7 @@ const UserCreate = ({ permissions, ...props }) => (
4853
source="name"
4954
defaultValue="Slim Shady"
5055
autoFocus
51-
validate={required()}
56+
validate={[required(), isValidName]}
5257
/>
5358
</FormTab>
5459
{permissions === 'admin' && (

packages/ra-core/src/form/validate.spec.ts

+74-20
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,99 @@ import {
1414
} from './validate';
1515

1616
describe('Validators', () => {
17-
const test = (validator, inputs, message) =>
18-
expect(
19-
inputs
20-
.map(input => validator(input, null))
21-
.map(error => (error && error.message ? error.message : error))
22-
).toEqual(Array(...Array(inputs.length)).map(() => message));
17+
const test = async (validator, inputs, message) => {
18+
const validationResults = await Promise.all<Error | undefined>(
19+
inputs.map(input => validator(input, null))
20+
).then(results =>
21+
results.map(error =>
22+
error && error.message ? error.message : error
23+
)
24+
);
25+
26+
expect(validationResults).toEqual(
27+
Array(...Array(inputs.length)).map(() => message)
28+
);
29+
};
2330

2431
describe('composeValidators', () => {
25-
it('Correctly composes validators passed as an array', () => {
26-
test(
27-
composeValidators([required(), minLength(5)]),
32+
const asyncSuccessfullValidator = async =>
33+
new Promise(resolve => resolve());
34+
const asyncFailedValidator = async =>
35+
new Promise(resolve => resolve('async'));
36+
37+
it('Correctly composes validators passed as an array', async () => {
38+
await test(
39+
composeValidators([
40+
required(),
41+
minLength(5),
42+
asyncSuccessfullValidator,
43+
]),
2844
[''],
2945
'ra.validation.required'
3046
);
31-
test(
32-
composeValidators([required(), minLength(5)]),
47+
await test(
48+
composeValidators([
49+
required(),
50+
asyncSuccessfullValidator,
51+
minLength(5),
52+
]),
3353
['abcd'],
3454
'ra.validation.minLength'
3555
);
36-
test(
37-
composeValidators([required(), minLength(5)]),
56+
await test(
57+
composeValidators([
58+
required(),
59+
asyncFailedValidator,
60+
minLength(5),
61+
]),
62+
['abcde'],
63+
'async'
64+
);
65+
await test(
66+
composeValidators([
67+
required(),
68+
minLength(5),
69+
asyncSuccessfullValidator,
70+
]),
3871
['abcde'],
3972
undefined
4073
);
4174
});
4275

43-
it('Correctly composes validators passed as many arguments', () => {
44-
test(
45-
composeValidators(required(), minLength(5)),
76+
it('Correctly composes validators passed as many arguments', async () => {
77+
await test(
78+
composeValidators(
79+
required(),
80+
minLength(5),
81+
asyncSuccessfullValidator
82+
),
4683
[''],
4784
'ra.validation.required'
4885
);
49-
test(
50-
composeValidators(required(), minLength(5)),
86+
await test(
87+
composeValidators(
88+
required(),
89+
asyncSuccessfullValidator,
90+
minLength(5)
91+
),
5192
['abcd'],
5293
'ra.validation.minLength'
5394
);
54-
test(
55-
composeValidators(required(), minLength(5)),
95+
await test(
96+
composeValidators(
97+
required(),
98+
asyncFailedValidator,
99+
minLength(5)
100+
),
101+
['abcde'],
102+
'async'
103+
);
104+
await test(
105+
composeValidators(
106+
required(),
107+
minLength(5),
108+
asyncSuccessfullValidator
109+
),
56110
['abcde'],
57111
undefined
58112
);

packages/ra-core/src/form/validate.ts

+37-9
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,46 @@ type Memoize = <T extends (...args: any[]) => any>(
6363
const memoize: Memoize = (fn: any) =>
6464
lodashMemoize(fn, (...args) => JSON.stringify(args));
6565

66+
const isFunction = value => typeof value === 'function';
67+
68+
// Compose multiple validators into a single one for use with final-form
69+
export const composeValidators = (...validators) => async (
70+
value,
71+
values,
72+
meta
73+
) => {
74+
const allValidators = (Array.isArray(validators[0])
75+
? validators[0]
76+
: validators
77+
).filter(isFunction);
78+
79+
for (const validator of allValidators) {
80+
const error = await validator(value, values, meta);
81+
82+
if (error) {
83+
return error;
84+
}
85+
}
86+
};
87+
6688
// Compose multiple validators into a single one for use with final-form
67-
export const composeValidators = (...validators) => (value, values, meta) => {
68-
const allValidators = Array.isArray(validators[0])
89+
export const composeSyncValidators = (...validators) => (
90+
value,
91+
values,
92+
meta
93+
) => {
94+
const allValidators = (Array.isArray(validators[0])
6995
? validators[0]
70-
: validators;
96+
: validators
97+
).filter(isFunction);
7198

72-
return allValidators.reduce(
73-
(error, validator) =>
74-
error ||
75-
(typeof validator === 'function' && validator(value, values, meta)),
76-
undefined
77-
);
99+
for (const validator of allValidators) {
100+
const error = validator(value, values, meta);
101+
102+
if (error) {
103+
return error;
104+
}
105+
}
78106
};
79107

80108
/**

packages/ra-ui-materialui/src/input/ArrayInput.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import * as React from 'react';
22
import { cloneElement, Children, FC, ReactElement } from 'react';
33
import PropTypes from 'prop-types';
4-
import { isRequired, FieldTitle, composeValidators, InputProps } from 'ra-core';
4+
import {
5+
isRequired,
6+
FieldTitle,
7+
composeSyncValidators,
8+
InputProps,
9+
} from 'ra-core';
510
import { useFieldArray } from 'react-final-form-arrays';
611
import { InputLabel, FormControl } from '@material-ui/core';
712

@@ -62,7 +67,7 @@ const ArrayInput: FC<ArrayInputProps> = ({
6267
...rest
6368
}) => {
6469
const sanitizedValidate = Array.isArray(validate)
65-
? composeValidators(validate)
70+
? composeSyncValidators(validate)
6671
: validate;
6772

6873
const fieldProps = useFieldArray(source, {

0 commit comments

Comments
 (0)