Skip to content

Commit

Permalink
Merge pull request #6140 from marmelab/fix-validation-documentation
Browse files Browse the repository at this point in the history
Fix validation documentation & Submission Errors Cannot Have Translatable Error Objects
  • Loading branch information
fzaninotto authored Apr 9, 2021
2 parents e045b98 + e97d95e commit 6f5e64b
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 30 deletions.
40 changes: 28 additions & 12 deletions docs/CreateEdit.md
Original file line number Diff line number Diff line change
Expand Up @@ -1215,12 +1215,17 @@ The value of the form `validate` prop must be a function taking the record as in
const validateUserCreation = (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = ['The firstName is required'];
errors.firstName = 'The firstName is required';
}
if (!values.age) {
errors.age = ['The age is required'];
// You can return translation keys
errors.age = 'ra.validation.required';
} else if (values.age < 18) {
errors.age = ['Must be over 18'];
// Or an object if the translation messages need parameters
errors.age = {
message: 'ra.validation.minValue',
args: { min: 18 }
};
}
return errors
};
Expand Down Expand Up @@ -1272,7 +1277,7 @@ const validateFirstName = [required(), minLength(2), maxLength(15)];
const validateEmail = email();
const validateAge = [number(), minValue(18)];
const validateZipCode = regex(/^\d{5}$/, 'Must be a valid Zip Code');
const validateSex = choices(['m', 'f'], 'Must be Male or Female');
const validateGender = choices(['m', 'f', 'nc'], 'Please choose one of the values');

export const UserCreate = (props) => (
<Create {...props}>
Expand All @@ -1281,10 +1286,11 @@ export const UserCreate = (props) => (
<TextInput label="Email" source="email" validate={validateEmail} />
<TextInput label="Age" source="age" validate={validateAge}/>
<TextInput label="Zip Code" source="zip" validate={validateZipCode}/>
<SelectInput label="Sex" source="sex" choices={[
<SelectInput label="Gender" source="gender" choices={[
{ id: 'm', name: 'Male' },
{ id: 'f', name: 'Female' },
]} validate={validateSex}/>
{ id: 'nc', name: 'Prefer not say' },
]} validate={validateGender}/>
</SimpleForm>
</Create>
);
Expand Down Expand Up @@ -1319,7 +1325,7 @@ const ageValidation = (value, allValues) => {
if (value < 18) {
return 'Must be over 18';
}
return [];
return undefined;
};

const validateFirstName = [required(), maxLength(15)];
Expand Down Expand Up @@ -1414,17 +1420,25 @@ You can validate the entire form data server-side by returning a Promise in the
const validateUserCreation = async (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = ['The firstName is required'];
errors.firstName = 'The firstName is required';
}
if (!values.age) {
errors.age = ['The age is required'];
errors.age = 'The age is required';
} else if (values.age < 18) {
errors.age = ['Must be over 18'];
errors.age = 'Must be over 18';
}

const isEmailUnique = await checkEmailIsUnique(values.userName);
const isEmailUnique = await checkEmailIsUnique(values.email);
if (!isEmailUnique) {
errors.email = ['Email already used'];
// Return a message directly
errors.email = 'Email already used';
// Or a translation key
errors.email = 'myapp.validation.email_not_unique';
// Or an object if the translation needs parameters
errors.email = {
message: 'myapp.validation.email_not_unique',
args: { email: values.email }
};
}
return errors
};
Expand Down Expand Up @@ -1515,6 +1529,8 @@ export const UserCreate = (props) => {

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

**Tip**: The returned validation errors might have any validation format we support (simple strings or object with message and args) for each key.

## Submit On Enter

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`:
Expand Down
40 changes: 31 additions & 9 deletions examples/simple/src/users/UserEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,26 @@ const EditActions = ({ basePath, data, hasShow }) => (
</TopToolbar>
);

const UserEdit = ({ permissions, ...props }) => (
<Edit
title={<UserTitle />}
aside={<Aside />}
actions={<EditActions />}
{...props}
>
const UserEditForm = ({ permissions, save, ...props }) => {
const newSave = values =>
new Promise((resolve, reject) => {
if (values.name === 'test') {
return resolve({
name: {
message: 'ra.validation.minLength',
args: { min: 10 },
},
});
}
return save(values);
});

return (
<TabbedForm
defaultValue={{ role: 'user' }}
toolbar={<UserEditToolbar />}
{...props}
save={newSave}
>
<FormTab label="user.form.summary" path="">
{permissions === 'admin' && <TextInput disabled source="id" />}
Expand All @@ -87,8 +97,20 @@ const UserEdit = ({ permissions, ...props }) => (
</FormTab>
)}
</TabbedForm>
</Edit>
);
);
};
const UserEdit = ({ permissions, ...props }) => {
return (
<Edit
title={<UserTitle />}
aside={<Aside />}
actions={<EditActions />}
{...props}
>
<UserEditForm permissions={permissions} />
</Edit>
);
};

UserEdit.propTypes = {
id: PropTypes.any.isRequired,
Expand Down
1 change: 0 additions & 1 deletion packages/ra-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"peerDependencies": {
"connected-react-router": "^6.5.2",
"final-form": "^4.20.2",
"final-form-submit-errors": "^0.1.2",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-final-form": "^6.5.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/form/FormWithRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';
import { useRef, useCallback, useEffect, useMemo } from 'react';
import { Form, FormProps, FormRenderProps } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
import { submitErrorsMutators } from 'final-form-submit-errors';

import useInitializeFormWithRecord from './useInitializeFormWithRecord';
import useWarnWhenUnsavedChanges from './useWarnWhenUnsavedChanges';
Expand All @@ -19,6 +18,7 @@ import { RedirectionSideEffect } from '../sideEffect';
import { useDispatch } from 'react-redux';
import { setAutomaticRefresh } from '../actions/uiActions';
import { FormContextProvider } from './FormContextProvider';
import submitErrorsMutators from './submitErrorsMutators';

/**
* Wrapper around react-final-form's Form to handle redirection on submit,
Expand Down
1 change: 0 additions & 1 deletion packages/ra-core/src/form/ValidationError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ interface Props {

const ValidationError: FunctionComponent<Props> = ({ error }) => {
const translate = useTranslate();

if ((error as ValidationErrorMessageWithArgs).message) {
const { message, args } = error as ValidationErrorMessageWithArgs;
return <>{translate(message, { _: message, ...args })}</>;
Expand Down
174 changes: 174 additions & 0 deletions packages/ra-core/src/form/submitErrorsMutators.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { getIn, setIn } from 'final-form';

import { resetSubmitErrors } from './submitErrorsMutators';

const makeFormState = ({
submitErrors,
submitError,
}: {
submitErrors?: any;
submitError?: any;
}) => ({
formState: {
submitError,
submitErrors,
},
});

describe('submitErrorsMutators', () => {
test('should ignore when no changes occur', () => {
const prev = {
value: 'hello',
};

const current = {
value: 'hello',
};

const state = makeFormState({
submitErrors: {
value: 'error',
},
});

resetSubmitErrors([{ prev, current }], state, { getIn, setIn });

expect(state.formState.submitErrors).toEqual({
value: 'error',
});
});

test('should reset errors for basic types', () => {
const prev = {
bool: true,
null: null,
number: 1,
string: 'one',
};

const current = {
bool: false,
null: undefined,
number: 2,
string: 'two',
};

const state = makeFormState({
submitErrors: {
bool: 'error',
null: 'error',
number: 'error',
string: 'error',
},
});

resetSubmitErrors([{ prev, current }], state, { getIn, setIn });

expect(state.formState.submitErrors).toEqual({});
});

test('should reset errors for nested objects', () => {
const prev = {
nested: {
deep: {
field: 'one',
other: {
field: 'two',
},
},
},
};

const current = {
nested: {
deep: {
field: 'two',
},
},
};

const state = makeFormState({
submitErrors: {
nested: {
deep: {
field: 'error',
other: 'error',
},
},
},
});

resetSubmitErrors([{ prev, current }], state, { getIn, setIn });

expect(state.formState.submitErrors).toEqual({});
});

test('should reset errors for arrays', () => {
const prev = {
array: [
{
some: [1, 2],
},
{
value: 'one',
},
1,
],
};

const current = {
array: [
{
some: [2],
},
{
value: 'one',
},
2,
],
};

const state = makeFormState({
submitErrors: {
array: [
{
some: ['error', 'error'],
},
{
value: 'error',
},
'error',
],
},
});

resetSubmitErrors([{ prev, current }], state, { getIn, setIn });

expect(state.formState.submitErrors).toEqual({
array: [undefined, { value: 'error' }],
});
});

test('should reset errors for validation error objects', () => {
const prev = {
field: 'aaaa',
};

const current = {
field: 'aaaaa',
};

const state = makeFormState({
submitErrors: {
field: {
message: 'ra.validation.min_length',
args: { min: 5 },
},
},
});

resetSubmitErrors([{ prev, current }], state, { getIn, setIn });

expect(state.formState.submitErrors).toEqual({});
});
});
Loading

0 comments on commit 6f5e64b

Please sign in to comment.