Skip to content

Commit 6f5e64b

Browse files
authored
Merge pull request #6140 from marmelab/fix-validation-documentation
Fix validation documentation & Submission Errors Cannot Have Translatable Error Objects
2 parents e045b98 + e97d95e commit 6f5e64b

File tree

9 files changed

+362
-30
lines changed

9 files changed

+362
-30
lines changed

docs/CreateEdit.md

+28-12
Original file line numberDiff line numberDiff line change
@@ -1215,12 +1215,17 @@ The value of the form `validate` prop must be a function taking the record as in
12151215
const validateUserCreation = (values) => {
12161216
const errors = {};
12171217
if (!values.firstName) {
1218-
errors.firstName = ['The firstName is required'];
1218+
errors.firstName = 'The firstName is required';
12191219
}
12201220
if (!values.age) {
1221-
errors.age = ['The age is required'];
1221+
// You can return translation keys
1222+
errors.age = 'ra.validation.required';
12221223
} else if (values.age < 18) {
1223-
errors.age = ['Must be over 18'];
1224+
// Or an object if the translation messages need parameters
1225+
errors.age = {
1226+
message: 'ra.validation.minValue',
1227+
args: { min: 18 }
1228+
};
12241229
}
12251230
return errors
12261231
};
@@ -1272,7 +1277,7 @@ const validateFirstName = [required(), minLength(2), maxLength(15)];
12721277
const validateEmail = email();
12731278
const validateAge = [number(), minValue(18)];
12741279
const validateZipCode = regex(/^\d{5}$/, 'Must be a valid Zip Code');
1275-
const validateSex = choices(['m', 'f'], 'Must be Male or Female');
1280+
const validateGender = choices(['m', 'f', 'nc'], 'Please choose one of the values');
12761281

12771282
export const UserCreate = (props) => (
12781283
<Create {...props}>
@@ -1281,10 +1286,11 @@ export const UserCreate = (props) => (
12811286
<TextInput label="Email" source="email" validate={validateEmail} />
12821287
<TextInput label="Age" source="age" validate={validateAge}/>
12831288
<TextInput label="Zip Code" source="zip" validate={validateZipCode}/>
1284-
<SelectInput label="Sex" source="sex" choices={[
1289+
<SelectInput label="Gender" source="gender" choices={[
12851290
{ id: 'm', name: 'Male' },
12861291
{ id: 'f', name: 'Female' },
1287-
]} validate={validateSex}/>
1292+
{ id: 'nc', name: 'Prefer not say' },
1293+
]} validate={validateGender}/>
12881294
</SimpleForm>
12891295
</Create>
12901296
);
@@ -1319,7 +1325,7 @@ const ageValidation = (value, allValues) => {
13191325
if (value < 18) {
13201326
return 'Must be over 18';
13211327
}
1322-
return [];
1328+
return undefined;
13231329
};
13241330

13251331
const validateFirstName = [required(), maxLength(15)];
@@ -1414,17 +1420,25 @@ You can validate the entire form data server-side by returning a Promise in the
14141420
const validateUserCreation = async (values) => {
14151421
const errors = {};
14161422
if (!values.firstName) {
1417-
errors.firstName = ['The firstName is required'];
1423+
errors.firstName = 'The firstName is required';
14181424
}
14191425
if (!values.age) {
1420-
errors.age = ['The age is required'];
1426+
errors.age = 'The age is required';
14211427
} else if (values.age < 18) {
1422-
errors.age = ['Must be over 18'];
1428+
errors.age = 'Must be over 18';
14231429
}
14241430

1425-
const isEmailUnique = await checkEmailIsUnique(values.userName);
1431+
const isEmailUnique = await checkEmailIsUnique(values.email);
14261432
if (!isEmailUnique) {
1427-
errors.email = ['Email already used'];
1433+
// Return a message directly
1434+
errors.email = 'Email already used';
1435+
// Or a translation key
1436+
errors.email = 'myapp.validation.email_not_unique';
1437+
// Or an object if the translation needs parameters
1438+
errors.email = {
1439+
message: 'myapp.validation.email_not_unique',
1440+
args: { email: values.email }
1441+
};
14281442
}
14291443
return errors
14301444
};
@@ -1515,6 +1529,8 @@ export const UserCreate = (props) => {
15151529

15161530
**Tip**: The shape of the returned validation errors must correspond to the form: a key needs to match a `source` prop.
15171531

1532+
**Tip**: The returned validation errors might have any validation format we support (simple strings or object with message and args) for each key.
1533+
15181534
## Submit On Enter
15191535

15201536
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`:

examples/simple/src/users/UserEdit.tsx

+31-9
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,26 @@ const EditActions = ({ basePath, data, hasShow }) => (
5353
</TopToolbar>
5454
);
5555

56-
const UserEdit = ({ permissions, ...props }) => (
57-
<Edit
58-
title={<UserTitle />}
59-
aside={<Aside />}
60-
actions={<EditActions />}
61-
{...props}
62-
>
56+
const UserEditForm = ({ permissions, save, ...props }) => {
57+
const newSave = values =>
58+
new Promise((resolve, reject) => {
59+
if (values.name === 'test') {
60+
return resolve({
61+
name: {
62+
message: 'ra.validation.minLength',
63+
args: { min: 10 },
64+
},
65+
});
66+
}
67+
return save(values);
68+
});
69+
70+
return (
6371
<TabbedForm
6472
defaultValue={{ role: 'user' }}
6573
toolbar={<UserEditToolbar />}
74+
{...props}
75+
save={newSave}
6676
>
6777
<FormTab label="user.form.summary" path="">
6878
{permissions === 'admin' && <TextInput disabled source="id" />}
@@ -87,8 +97,20 @@ const UserEdit = ({ permissions, ...props }) => (
8797
</FormTab>
8898
)}
8999
</TabbedForm>
90-
</Edit>
91-
);
100+
);
101+
};
102+
const UserEdit = ({ permissions, ...props }) => {
103+
return (
104+
<Edit
105+
title={<UserTitle />}
106+
aside={<Aside />}
107+
actions={<EditActions />}
108+
{...props}
109+
>
110+
<UserEditForm permissions={permissions} />
111+
</Edit>
112+
);
113+
};
92114

93115
UserEdit.propTypes = {
94116
id: PropTypes.any.isRequired,

packages/ra-core/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"peerDependencies": {
5656
"connected-react-router": "^6.5.2",
5757
"final-form": "^4.20.2",
58-
"final-form-submit-errors": "^0.1.2",
5958
"react": "^16.9.0 || ^17.0.0",
6059
"react-dom": "^16.9.0 || ^17.0.0",
6160
"react-final-form": "^6.5.2",

packages/ra-core/src/form/FormWithRedirect.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as React from 'react';
22
import { useRef, useCallback, useEffect, useMemo } from 'react';
33
import { Form, FormProps, FormRenderProps } from 'react-final-form';
44
import arrayMutators from 'final-form-arrays';
5-
import { submitErrorsMutators } from 'final-form-submit-errors';
65

76
import useInitializeFormWithRecord from './useInitializeFormWithRecord';
87
import useWarnWhenUnsavedChanges from './useWarnWhenUnsavedChanges';
@@ -19,6 +18,7 @@ import { RedirectionSideEffect } from '../sideEffect';
1918
import { useDispatch } from 'react-redux';
2019
import { setAutomaticRefresh } from '../actions/uiActions';
2120
import { FormContextProvider } from './FormContextProvider';
21+
import submitErrorsMutators from './submitErrorsMutators';
2222

2323
/**
2424
* Wrapper around react-final-form's Form to handle redirection on submit,

packages/ra-core/src/form/ValidationError.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ interface Props {
1212

1313
const ValidationError: FunctionComponent<Props> = ({ error }) => {
1414
const translate = useTranslate();
15-
1615
if ((error as ValidationErrorMessageWithArgs).message) {
1716
const { message, args } = error as ValidationErrorMessageWithArgs;
1817
return <>{translate(message, { _: message, ...args })}</>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { getIn, setIn } from 'final-form';
2+
3+
import { resetSubmitErrors } from './submitErrorsMutators';
4+
5+
const makeFormState = ({
6+
submitErrors,
7+
submitError,
8+
}: {
9+
submitErrors?: any;
10+
submitError?: any;
11+
}) => ({
12+
formState: {
13+
submitError,
14+
submitErrors,
15+
},
16+
});
17+
18+
describe('submitErrorsMutators', () => {
19+
test('should ignore when no changes occur', () => {
20+
const prev = {
21+
value: 'hello',
22+
};
23+
24+
const current = {
25+
value: 'hello',
26+
};
27+
28+
const state = makeFormState({
29+
submitErrors: {
30+
value: 'error',
31+
},
32+
});
33+
34+
resetSubmitErrors([{ prev, current }], state, { getIn, setIn });
35+
36+
expect(state.formState.submitErrors).toEqual({
37+
value: 'error',
38+
});
39+
});
40+
41+
test('should reset errors for basic types', () => {
42+
const prev = {
43+
bool: true,
44+
null: null,
45+
number: 1,
46+
string: 'one',
47+
};
48+
49+
const current = {
50+
bool: false,
51+
null: undefined,
52+
number: 2,
53+
string: 'two',
54+
};
55+
56+
const state = makeFormState({
57+
submitErrors: {
58+
bool: 'error',
59+
null: 'error',
60+
number: 'error',
61+
string: 'error',
62+
},
63+
});
64+
65+
resetSubmitErrors([{ prev, current }], state, { getIn, setIn });
66+
67+
expect(state.formState.submitErrors).toEqual({});
68+
});
69+
70+
test('should reset errors for nested objects', () => {
71+
const prev = {
72+
nested: {
73+
deep: {
74+
field: 'one',
75+
other: {
76+
field: 'two',
77+
},
78+
},
79+
},
80+
};
81+
82+
const current = {
83+
nested: {
84+
deep: {
85+
field: 'two',
86+
},
87+
},
88+
};
89+
90+
const state = makeFormState({
91+
submitErrors: {
92+
nested: {
93+
deep: {
94+
field: 'error',
95+
other: 'error',
96+
},
97+
},
98+
},
99+
});
100+
101+
resetSubmitErrors([{ prev, current }], state, { getIn, setIn });
102+
103+
expect(state.formState.submitErrors).toEqual({});
104+
});
105+
106+
test('should reset errors for arrays', () => {
107+
const prev = {
108+
array: [
109+
{
110+
some: [1, 2],
111+
},
112+
{
113+
value: 'one',
114+
},
115+
1,
116+
],
117+
};
118+
119+
const current = {
120+
array: [
121+
{
122+
some: [2],
123+
},
124+
{
125+
value: 'one',
126+
},
127+
2,
128+
],
129+
};
130+
131+
const state = makeFormState({
132+
submitErrors: {
133+
array: [
134+
{
135+
some: ['error', 'error'],
136+
},
137+
{
138+
value: 'error',
139+
},
140+
'error',
141+
],
142+
},
143+
});
144+
145+
resetSubmitErrors([{ prev, current }], state, { getIn, setIn });
146+
147+
expect(state.formState.submitErrors).toEqual({
148+
array: [undefined, { value: 'error' }],
149+
});
150+
});
151+
152+
test('should reset errors for validation error objects', () => {
153+
const prev = {
154+
field: 'aaaa',
155+
};
156+
157+
const current = {
158+
field: 'aaaaa',
159+
};
160+
161+
const state = makeFormState({
162+
submitErrors: {
163+
field: {
164+
message: 'ra.validation.min_length',
165+
args: { min: 5 },
166+
},
167+
},
168+
});
169+
170+
resetSubmitErrors([{ prev, current }], state, { getIn, setIn });
171+
172+
expect(state.formState.submitErrors).toEqual({});
173+
});
174+
});

0 commit comments

Comments
 (0)