Skip to content

Commit

Permalink
Merge pull request #4458 from marmelab/custom-submit-functions
Browse files Browse the repository at this point in the history
Allow to keep form validation when using custom save buttons
  • Loading branch information
fzaninotto authored Mar 5, 2020
2 parents a503ade + c55b400 commit 4cfd65c
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 167 deletions.
80 changes: 38 additions & 42 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ The author of `redux-form` has written a new Form library for React called `reac

The next sections highlight changes that you must do to your code as a consequence of switching to `react-final-form`.

## Custom Form Toolbar or Buttons Must Use New `handleSubmit` Signature
## Custom Form Toolbar or Buttons Must Use New `handleSubmit` Signature or must Use `onSave`

If you were using custom buttons (to alter the form values before submit for example), you'll need to update your code. In `react-admin` v2, the form toolbar and its buttons used to receive `handleSubmit` and `handleSubmitWithRedirect` props. These props accepted functions which were called with the form values.

Expand All @@ -112,9 +112,7 @@ The migration to `react-final-form` changes their signature and behavior to the
- `handleSubmit`: accepts no arguments, and will submit the form with its current values immediately
- `handleSubmitWithRedirect` accepts a custom redirect, and will submit the form with its current values immediately

Here's how to migrate the *Altering the Form Values before Submitting* example from the documentation, in two variants:

1. Using the `react-final-form` hook API to send change events
Here's how to migrate the *Altering the Form Values before Submitting* example from the documentation:

```jsx
import React, { useCallback } from 'react';
Expand All @@ -139,53 +137,51 @@ const SaveWithNoteButton = ({ handleSubmit, handleSubmitWithRedirect, ...props }
};
```

2. Using react-admin hooks to run custom mutations
The override of these functions has now a huge drawback, which makes it impractical: by skipping the default `handleSubmitWithRedirect`, the button doesn't trigger form validation. And unfortunately, react-final-form doesn't provide a way to trigger form validation manually.
That's why react-admin now provides a way to override just the data provider call and its side effect called `onSave`.

The `onSave` value should be a function expecting 2 arguments: the form values to save, and the redirection to perform.

For instance, in the `simple` example:
Here's how to migrate the *Using `onSave` To Alter the Form Submission Behavior* example from the documentation:

```jsx
import React, { useCallback } from 'react';
import { useFormState } from 'react-final-form';
import { SaveButton, Toolbar, useCreate, useRedirect, useNotify } from 'react-admin';

import {
SaveButton,
Toolbar,
useCreate,
useRedirect,
useNotify,
} from 'react-admin';
const SaveWithNoteButton = props => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;

const formState = useFormState();
const handleClick = useCallback(() => {
if (!formState.valid) {
return;
}

create(
{
payload: {
data: { ...formState.values, average_note: 10 },
const { basePath } = props;
const handleSave = useCallback(
(values, redirect) => {
create(
{
payload: { data: { ...values, average_note: 10 } },
},
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
);
}, [
formState.valid,
formState.values,
create,
notify,
redirectTo,
redirect,
basePath,
]);

return <SaveButton {...props} handleSubmitWithRedirect={handleClick} />;
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
);
},
[create, notify, redirectTo, basePath]
);
// set onSave props instead of handleSubmitWithRedirect
return <SaveButton {...props} onSave={handleSave} />;
};
```

Expand Down
180 changes: 116 additions & 64 deletions docs/CreateEdit.md
Original file line number Diff line number Diff line change
Expand Up @@ -1230,85 +1230,42 @@ export const UserEdit = ({ permissions, ...props }) =>
```
{% endraw %}
## Altering the Form Values before Submitting
## Altering the Form Values Before Submitting
Sometimes, you may want your custom action to alter the form values before actually sending them to the `dataProvider`.
For those cases, you should know that every button inside a form [Toolbar](#toolbar) receive two props:
Sometimes, you may want to alter the form values before actually sending them to the `dataProvider`. For those cases, you should know that every button inside a form [Toolbar](#toolbar) receive two props:
- `handleSubmit` which calls the default form save method
- `handleSubmitWithRedirect` which calls the default form save method but allows to specify a custom redirection
Knowing this, there are two ways to alter the form values before submit:
1. Using react-final-form API to send change events
* `handleSubmit` which calls the default form save method (provided by react-final-form)
* `handleSubmitWithRedirect` which calls the default form save method and allows to specify a custom redirection
Decorating `handleSubmitWithRedirect` with your own logic allows you to alter the form values before submitting. For instance, to set the `average_note` field value just before submission:
```jsx
import React, { useCallback } from 'react';
import { useForm } from 'react-final-form';
import { SaveButton, Toolbar, useCreate, useRedirect, useNotify } from 'react-admin';
import {
SaveButton,
Toolbar,
useCreate,
useRedirect,
useNotify,
} from 'react-admin';
const SaveWithNoteButton = ({ handleSubmitWithRedirect, ...props }) => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;
const form = useForm();
const handleClick = useCallback(() => {
// change the average_note field value
form.change('average_note', 10);
handleSubmitWithRedirect('edit');
}, [form]);
return <SaveButton {...props} handleSubmitWithRedirect={handleClick} />;
};
```
2. Using react-admin hooks to run custom mutations
For instance, in the `simple` example:
```jsx
import React, { useCallback } from 'react';
import { useFormState } from 'react-final-form';
import { SaveButton, Toolbar, useCreate, useRedirect, useNotify } from 'react-admin';
const SaveWithNoteButton = props => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;
const formState = useFormState();
const handleClick = useCallback(() => {
if (!formState.valid) {
return;
}
create(
{
payload: { data: { ...formState.values, average_note: 10 } },
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
);
}, [
formState.valid,
formState.values,
create,
notify,
redirectTo,
redirect,
basePath,
]);
// override handleSubmitWithRedirect with custom logic
return <SaveButton {...props} handleSubmitWithRedirect={handleClick} />;
};
```
Expand All @@ -1333,4 +1290,99 @@ const PostCreateToolbar = props => (
);
```
**Note**: This technique will not trigger a form validation pass.
**Tip**: Which one of `handleSubmit` and `handleSubmitWithRedirect` should you override? If you want to keep the redirection, override `handleSubmitWithRedirect` just like in the previous example. If you want to disable redirection, or handle it yourself, override `handleSubmit`.
## Using `onSave` To Alter the Form Submission Behavior
The previous technique works well for altering values. But you may want to call a route before submission, or submit the form to different dataProvider methods/resources depending on the form values. And in this case, wrapping `handleSubmitWithRedirect` does not work, because you don't have control on the submission itself.
Instead of *decorating* `handleSubmitWithRedirect`, you can *replace* it, and do the API call manually. You don't have to change anything in the form values in that case. So the previous example can be rewritten as:
```jsx
import React, { useCallback } from 'react';
import { useFormState } from 'react-final-form';
import {
SaveButton,
Toolbar,
useCreate,
useRedirect,
useNotify,
} from 'react-admin';
const SaveWithNoteButton = props => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;
// get values from the form
const formState = useFormState();
const handleClick = useCallback(
() => {
// call dataProvider.create() manually
create(
{
payload: { data: { ...formState.values, average_note: 10 } },
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
);
},
[create, notify, redirectTo, basePath, formState, redirect]
);
return <SaveButton {...props} handleSubmitWithRedirect={handleClick} />;
};
```
This technique has a huge drawback, which makes it impractical: by skipping the default `handleSubmitWithRedirect`, this button doesn't trigger form validation. And unfortunately, react-final-form doesn't provide a way to trigger form validation manually.
That's why react-admin provides a way to override just the data provider call and its side effects. It's called `onSave`, and here is how you would use it in the previous use case:
```jsx
import React, { useCallback } from 'react';
import {
SaveButton,
Toolbar,
useCreate,
useRedirect,
useNotify,
} from 'react-admin';
const SaveWithNoteButton = props => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath } = props;
const handleSave = useCallback(
(values, redirect) => {
create(
{
payload: { data: { ...values, average_note: 10 } },
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
);
},
[create, notify, redirectTo, basePath]
);
// set onSave props instead of handleSubmitWithRedirect
return <SaveButton {...props} onSave={handleSave} />;
};
```
The `onSave` value should be a function expecting 2 arguments: the form values to save, and the redirection to perform.
54 changes: 22 additions & 32 deletions examples/simple/src/posts/PostCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,36 @@ import {
useRedirect,
useNotify,
} from 'react-admin'; // eslint-disable-line import/no-unresolved
import { useFormState, FormSpy } from 'react-final-form';
import { FormSpy } from 'react-final-form';

const SaveWithNoteButton = props => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;
const { basePath } = props;

const formState = useFormState();
const handleClick = useCallback(() => {
if (!formState.valid) {
return;
}

create(
{
payload: {
data: { ...formState.values, average_note: 10 },
},
},
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
const handleSave = useCallback(
(values, redirect) => {
create(
{
payload: {
data: { ...values, average_note: 10 },
},
},
}
);
}, [
formState.valid,
formState.values,
create,
notify,
redirectTo,
redirect,
basePath,
]);
{
onSuccess: ({ data: newRecord }) => {
notify('ra.notification.created', 'info', {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
}
);
},
[create, notify, redirectTo, basePath]
);

return <SaveButton {...props} handleSubmitWithRedirect={handleClick} />;
return <SaveButton {...props} onSave={handleSave} />;
};

const PostCreateToolbar = props => (
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/src/posts/PostEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const EditActions = ({ basePath, data, hasShow }) => (

const PostEdit = ({ permissions, ...props }) => (
<Edit title={<PostTitle />} actions={<EditActions />} {...props}>
<TabbedForm defaultValue={{ average_note: 0 }}>
<TabbedForm initialValues={{ average_note: 0 }}>
<FormTab label="post.form.summary">
<TextInput disabled source="id" />
<TextInput source="title" validate={required()} resettable />
Expand Down
Loading

0 comments on commit 4cfd65c

Please sign in to comment.