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

Fix validation documentation & Submission Errors Cannot Have Translatable Error Objects #6140

Merged
merged 3 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
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 validateGenre = 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="Genre" source="genre" choices={[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo Genre => Gender

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't even wait for merged docs anymore 😂 Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to be more inclusive and ended up being even less than before 😱

{ id: 'm', name: 'Male' },
{ id: 'f', name: 'Female' },
]} validate={validateSex}/>
{ id: 'nc', name: 'Prefer not say' },
]} validate={validateGenre}/>
</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