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

Allow to keep form validation when using custom save buttons #4458

Merged
merged 19 commits into from
Mar 5, 2020
Merged
Show file tree
Hide file tree
Changes from 16 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
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