Skip to content

Commit 312867b

Browse files
authoredJun 5, 2023
Merge pull request #8969 from marmelab/doc-add-autosave
[Doc] Add `<AutoSave>` documentation
2 parents 5b387fd + 72125fd commit 312867b

15 files changed

+1658
-118
lines changed
 

‎docs/AccordionForm.md

+454-8
Large diffs are not rendered by default.

‎docs/AutoSave.md

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
layout: default
3+
title: "The AutoSave Component"
4+
---
5+
6+
# `<AutoSave>`
7+
8+
This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component enables autosaving of the form. Alternative to [`<SaveButton>`](./SaveButton.md), it's ideal for long data entry tasks, and reduces the risk of data loss.
9+
10+
<video controls autoplay playsinline muted loop>
11+
<source src="./img/AutoSave.webm" type="video/webm"/>
12+
<source src="./img/AutoSave.mp4" type="video/mp4"/>
13+
Your browser does not support the video tag.
14+
</video>
15+
16+
Test it live on [the Enterprise Edition Storybook](https://react-admin.github.io/ra-enterprise/?path=/story/ra-form-layout-autosave-optimistic--in-simple-form).
17+
18+
## Usage
19+
20+
Put `<AutoSave>` inside a react-admin form ([`<SimpleForm>`](./SimpleForm.md), [`<TabbedForm>`](./TabbedForm.md), [`<LongForm>`](./LongForm.md), etc.), for instance in a custom toolbar. The component renders nothing by default. It will save the current form values 3 seconds after the last change, and render a message when the save succeeds or fails.
21+
22+
Note that you **must** set the `<Form resetOptions>` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.
23+
24+
If you're using it in an `<Edit>` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `<AutoSave>` doesn't work with the default `mutationMode="undoable"`.
25+
26+
{% raw %}
27+
```tsx
28+
import { AutoSave } from '@react-admin/ra-form-layout';
29+
import { Edit, SimpleForm, TextInput, DateInput, SelectInput, Toolbar } from 'react-admin';
30+
31+
const AutoSaveToolbar = () => (
32+
<Toolbar>
33+
<AutoSave />
34+
</Toolbar>
35+
);
36+
37+
const PersonEdit = () => (
38+
<Edit mutationMode="optimistic">
39+
<SimpleForm
40+
resetOptions={{ keepDirtyValues: true }}
41+
toolbar={AutoSaveToolbar}
42+
>
43+
<TextInput source="first_name" />
44+
<TextInput source="last_name" />
45+
<DateInput source="dob" />
46+
<SelectInput source="sex" choices={[
47+
{ id: 'male', name: 'Male' },
48+
{ id: 'female', name: 'Female' },
49+
]}/>
50+
</SimpleForm>
51+
</Edit>
52+
);
53+
```
54+
{% endraw %}
55+
56+
The app will save the current form values after 3 seconds of inactivity.
57+
58+
You can use a toolbar containing both a `<SaveButton>` and an `<AutoSave>` component. The `<SaveButton>` will let the user save the form immediately, while the `<AutoSave>` will save the form after 3 seconds of inactivity.
59+
60+
```tsx
61+
const AutoSaveToolbar = () => (
62+
<Toolbar>
63+
<SaveButton />
64+
<AutoSave />
65+
</Toolbar>
66+
);
67+
```
68+
69+
## Props
70+
71+
| Prop | Required | Type | Default | Description |
72+
| ----------- | -------- | -------------- | ------- | ----------------------------------------- |
73+
| `debounce` | Optional | `number` | 3000 | The interval in ms between two saves |
74+
| `onSuccess` | Optional | `function` | - | A callback to call when the save succeeds |
75+
| `onError` | Optional | `function` | - | A callback to call when the save fails |
76+
| `transform` | Optional | `function` | - | A function to transform the data before saving |
77+
78+
## `useAutoSave`
79+
80+
If you want an autosave feature with another user interface, you can leverage the `useAutoSave` hook. It's used internally by `<AutoSave>`, and has the same constraints (it works for the `pessimistic` and `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) but not for the `undoable`).
81+
82+
`useAutoSave` expects an options argument with the following fields, all optional:
83+
84+
- `debounce`: The interval in ms between two saves. Defaults to 3000 (3s).
85+
- `onSuccess`: A callback to call when the save request succeeds.
86+
- `onError`: A callback to call when the save request fails.
87+
- `transform`: A function to transform the data before saving.
88+
89+
Note that you **must** add the `resetOptions` prop with `{ keepDirtyValues: true }` to avoid having the user changes overridden by the latest update operation result.
90+
91+
{% raw %}
92+
```tsx
93+
import { useAutoSave } from '@react-admin/ra-form-layout';
94+
import { Edit, SaveButton, SimpleForm, TextInput, Toolbar } from 'react-admin';
95+
96+
const AutoSave = () => {
97+
const [lastSave, setLastSave] = useState();
98+
const [error, setError] = useState();
99+
useAutoSave({
100+
interval: 5000,
101+
onSuccess: () => setLastSave(new Date()),
102+
onError: error => setError(error),
103+
});
104+
return (
105+
<div>
106+
{lastSave && <p>Saved at {lastSave.toLocaleString()}</p>}
107+
{error && <p>Error: {error}</p>}
108+
</div>
109+
);
110+
};
111+
112+
const AutoSaveToolbar = () => (
113+
<Toolbar>
114+
<SaveButton />
115+
<AutoSave />
116+
</Toolbar>
117+
);
118+
119+
const PostEdit = () => (
120+
<Edit mutationMode="optimistic">
121+
<SimpleForm
122+
resetOptions={{ keepDirtyValues: true }}
123+
toolbar={AutoSaveToolbar}
124+
>
125+
<TextInput source="title" />
126+
<TextInput source="teaser" />
127+
</SimpleForm>
128+
</Edit>
129+
);
130+
```
131+
{% endraw %}
132+
133+
`usaAutoSave` returns a boolean indicating whether the form is currently being saved.
134+
135+
```jsx
136+
const isSaving = useAutoSave({
137+
interval: 5000,
138+
onSuccess: () => setLastSave(new Date()),
139+
onError: error => setError(error),
140+
});
141+
```

‎docs/EditTutorial.md

+55
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,15 @@ export const PostCreate = () => (
545545

546546
## Validating User Input
547547

548+
React-admin supports the most common validation strategies:
549+
550+
* [per field validators](./Validation.md#per-input-validation-built-in-field-validators),
551+
* [form validation](./Validation.md#global-validation),
552+
* [validation schema powered by yup or zod](./Validation.html#schema-validation),
553+
* [server-side validation](./Validation.md#server-side-validation).
554+
555+
![Validation example](./img/validation.png)
556+
548557
Form validation deserves a section of its own ; check [the Validation chapter](./Validation.md) for more details.
549558

550559
## Altering the Form Values Before Submitting
@@ -751,6 +760,52 @@ export const PostEdit = () => (
751760
);
752761
```
753762

763+
## AutoSave
764+
765+
In forms where users may spend a lot of time, it's a good idea to save the form automatically after a few seconds of inactivity. You can auto save the form content by using [the `<AutoSave>` component](./AutoSave.md).
766+
767+
<video controls autoplay playsinline muted loop>
768+
<source src="./img/AutoSave.webm" type="video/webm"/>
769+
<source src="./img/AutoSave.mp4" type="video/mp4"/>
770+
Your browser does not support the video tag.
771+
</video>
772+
773+
{% raw %}
774+
```tsx
775+
import { AutoSave } from '@react-admin/ra-form-layout';
776+
import { Edit, SimpleForm, TextInput, DateInput, SelectInput, Toolbar } from 'react-admin';
777+
778+
const AutoSaveToolbar = () => (
779+
<Toolbar>
780+
<AutoSave />
781+
</Toolbar>
782+
);
783+
784+
const PersonEdit = () => (
785+
<Edit mutationMode="optimistic">
786+
<SimpleForm
787+
resetOptions={{ keepDirtyValues: true }}
788+
toolbar={AutoSaveToolbar}
789+
>
790+
<TextInput source="first_name" />
791+
<TextInput source="last_name" />
792+
<DateInput source="dob" />
793+
<SelectInput source="sex" choices={[
794+
{ id: 'male', name: 'Male' },
795+
{ id: 'female', name: 'Female' },
796+
]}/>
797+
</SimpleForm>
798+
</Edit>
799+
);
800+
```
801+
{% endraw %}
802+
803+
Note that you **must** set the `<SimpleForm resetOptions>` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.
804+
805+
If you're using it in an `<Edit>` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `<AutoSave>` doesn't work with the default `mutationMode="undoable"`.
806+
807+
Check [the `<AutoSave>` component](./AutoSave.md) documentation for more details.
808+
754809
## Adding Fields With Labels
755810

756811
All react-admin inputs handle the display of their label by wrapping their content inside a `<Labeled>` component.

‎docs/Features.md

+58-30
Original file line numberDiff line numberDiff line change
@@ -228,21 +228,19 @@ const BookList = () => (
228228
Your browser does not support the video tag.
229229
</video>
230230

231-
React-admin supports **one-to-many**, **many-to-one**, **one-to-one**, and **many-to-many relationships**. Check the following components to learn more about relationships:
232-
233-
- [`ReferenceField`](./ReferenceField.md)
234-
- [`ReferenceArrayField`](./ReferenceArrayField.md)
235-
- [`ReferenceManyField`](./ReferenceManyField.md)
236-
- [`ReferenceManyCount`](./ReferenceManyCount.md)
237-
- [`ReferenceManyToManyField`](./ReferenceManyToManyField.md)
238-
- [`ReferenceOneField`](./ReferenceOneField.md)
239-
- [`ReferenceInput`](./ReferenceInput.md)
240-
- [`ReferenceArrayInput`](./ReferenceArrayInput.md)
241-
- [`ReferenceManyInput`](./ReferenceManyInput.md)
242-
- [`ReferenceManyToManyInput`](./ReferenceManyToManyInput.md)
243-
- [`ReferenceOneInput`](./ReferenceOneInput.md)
244-
245-
The [Fields For Relationships](./FieldsForRelationships.md) page lists all reference fields together with their common usage.
231+
React-admin supports **one-to-many**, **many-to-one**, **one-to-one**, and **many-to-many relationships**. The [Fields For Relationships](./FieldsForRelationships.md) page lists all reference fields together with their common usage. Check the following components to learn more about relationships:
232+
233+
- [`<ReferenceField>`](./ReferenceField.md)
234+
- [`<ReferenceArrayField>`](./ReferenceArrayField.md)
235+
- [`<ReferenceManyField>`](./ReferenceManyField.md)
236+
- [`<ReferenceManyCount>`](./ReferenceManyCount.md)
237+
- [`<ReferenceManyToManyField>`](./ReferenceManyToManyField.md)
238+
- [`<ReferenceOneField>`](./ReferenceOneField.md)
239+
- [`<ReferenceInput>`](./ReferenceInput.md)
240+
- [`<ReferenceArrayInput>`](./ReferenceArrayInput.md)
241+
- [`<ReferenceManyInput>`](./ReferenceManyInput.md)
242+
- [`<ReferenceManyToManyInput>`](./ReferenceManyToManyInput.md)
243+
- [`<ReferenceOneInput>`](./ReferenceOneInput.md)
246244

247245
Reference components are a tremendous development accelerator for complex frontend features. They also liberate the backend developers from the burden of implementing complex joins.
248246

@@ -293,9 +291,9 @@ Guesser components start by fetching data from the API, analyzing the shape of t
293291

294292
Check the following components to learn more about guessers:
295293

296-
- [`ListGuesser`](./ListGuesser.md)
297-
- [`EditGuesser`](./EditGuesser.md)
298-
- [`ShowGuesser`](./ShowGuesser.md)
294+
- [`<ListGuesser>`](./ListGuesser.md)
295+
- [`<EditGuesser>`](./EditGuesser.md)
296+
- [`<ShowGuesser>`](./ShowGuesser.md)
299297

300298
## Search & Filtering
301299

@@ -422,9 +420,9 @@ Check the [Building A Custom Filter Tutorial](./FilteringTutorial.md#building-a-
422420

423421
Many admin apps let users perform complex tasks implying the update of many fields and records. To allow such complex workflows, developers must be able to build sophisticated forms, with elaborate validation rules.
424422

425-
React-admin offers a **rich set of input components** to build forms, powered by [Material UI](https://mui.com/material-ui/getting-started/overview/) and [react-hook-form](https://react-hook-form.com/). React-admin's form components also take care of binding the form values to the record being edited and validating the form inputs.
423+
React-admin offers a **rich set of input components and form layouts** to build forms, powered by [Material UI](https://mui.com/material-ui/getting-started/overview/) and [react-hook-form](https://react-hook-form.com/). React-admin's form components also take care of binding the form values to the record being edited and validating the form inputs.
426424

427-
For instance, here is how to group inputs into tabs using `<TabbedForm>`:
425+
For instance, here is how to build a tabbed form for editing a blog post:
428426

429427
```jsx
430428
import {
@@ -479,18 +477,21 @@ export const PostEdit = () => (
479477
Your browser does not support the video tag.
480478
</video>
481479

480+
### Form Layouts
482481

483-
React-admin offers, out of the box, several **form layouts**:
482+
React-admin offers, out of the box, several form layouts:
484483

485-
- [`SimpleForm`](./SimpleForm.md) for a single-column layout
486-
- [`TabbedForm`](./TabbedForm.md) for a tabbed layout
487-
- [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections
488-
- [`LongForm`](./LongForm.md) for long forms with a navigation sidebar
489-
- [`WizardForm`](./WizardForm.md) for multi-step forms
490-
- [`EditInDialog`](./EditInDialog.md) for sub-forms in a modal dialog
484+
- [`<SimpleForm>`](./SimpleForm.md) for a single-column layout
485+
- [`<TabbedForm>`](./TabbedForm.md) for a tabbed layout
486+
- [`<AccordionForm>`](./AccordionForm.md) for long forms with collapsible sections
487+
- [`<LongForm>`](./LongForm.md) for long forms with a navigation sidebar
488+
- [`<WizardForm>`](./WizardForm.md) for multi-step forms
489+
- [`<EditInDialog>`](./EditInDialog.md) for sub-forms in a modal dialog
491490
- and [`Form`](./Form.md), a headless component to use as a base for your custom layouts
492491

493-
Inside forms, you can use [react-admin input components](./Inputs.md), designed for many types of data:
492+
### Input Components
493+
494+
Inside forms, you can use specialize [input components](./Inputs.md), designed for many types of data:
494495

495496
| Data Type | Example value | Input Components |
496497
|-----------------------|--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -514,7 +515,9 @@ Inside forms, you can use [react-admin input components](./Inputs.md), designed
514515
| Translations | `{ en: 'Hello', fr: 'Bonjour' }` | [`<TranslatableInputs>`](./TranslatableInputs.md) |
515516
| Related records | `[{ id: 42, title: 'Hello' }, { id: 43, title: 'World' }]` | [`<ReferenceManyInput>`](./ReferenceManyInput.md), [`<ReferenceManyToManyInput>`](./ReferenceManyToManyInput.md), [`<ReferenceOneInput>`](./ReferenceOneInput.md) |
516517

517-
You can build **dependent inputs**, using the [react-hook-form's `useWatch` hook](https://react-hook-form.com/api/usewatch). For instance, here is a `CityInput` that displays the cities of the selected country:
518+
### Dependent Inputs
519+
520+
You can build dependent inputs, using the [react-hook-form's `useWatch` hook](https://react-hook-form.com/api/usewatch). For instance, here is a `CityInput` that displays the cities of the selected country:
518521

519522
```jsx
520523
import * as React from 'react';
@@ -553,7 +556,20 @@ const OrderEdit = () => (
553556
export default OrderEdit;
554557
```
555558

556-
For validation, you can use react-admin's [per field validators](https://marmelab.com/react-admin/Validation.html#per-input-validation-built-in-field-validators), or a [validation schema powered by yup or zod](https://marmelab.com/react-admin/Validation.html#schema-validation). Here is an example of per-field validation:
559+
### Validation
560+
561+
React-admin ships with a powerful and versatile validation engine.
562+
563+
![Validation example](./img/validation.png)
564+
565+
React-admin forms support the most common validation strategies:
566+
567+
* [per field validators](./Validation.md#per-input-validation-built-in-field-validators),
568+
* [form validation](./Validation.md#global-validation),
569+
* [validation schema powered by yup or zod](./Validation.html#schema-validation),
570+
* [server-side validation](./Validation.md#server-side-validation).
571+
572+
Here is an example of per-field validation:
557573

558574
```jsx
559575
import {
@@ -591,6 +607,18 @@ export const UserCreate = () => (
591607
);
592608
```
593609

610+
### AutoSave
611+
612+
React-admin lets you build forms saving changes automatically with [`<AutoSave>`](./AutoSave.md), so that users never lose their changes.
613+
614+
<video controls autoplay playsinline muted loop>
615+
<source src="./img/AutoSave.webm" type="video/webm"/>
616+
<source src="./img/AutoSave.mp4" type="video/mp4"/>
617+
Your browser does not support the video tag.
618+
</video>
619+
620+
### JSON Schema Forms
621+
594622
Finally, you can generate entire **forms based on a JSON schema**, using the [`<JsonSchemaForm>` component](./JsonSchemaForm.md).
595623

596624
{% raw %}

‎docs/Form.md

+64-15
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The `<Form>` component creates a `<form>` to edit a record, and renders its chil
1111

1212
## Usage
1313

14-
Use `<Form>` to build completely custom form layouts. Don't forget to include a submit button:
14+
Use `<Form>` to build completely custom form layouts. Don't forget to include a submit button (or react-admin's [`<SaveButton>`](./SaveButton.md)) to actually save the record.
1515

1616
```jsx
1717
import { Create, Form, TextInput, RichTextInput, SaveButton } from 'react-admin';
@@ -41,25 +41,22 @@ export const PostCreate = () => (
4141

4242
`<Form>` calls react-hook-form's `useForm` hook, and places the result in a `FormProvider` component. This means you can take advantage of the [`useFormContext`](https://react-hook-form.com/api/useformcontext) and [`useFormState`](https://react-hook-form.com/api/useformstate) hooks to access the form state.
4343

44+
## Props
45+
4446
Here are all the props you can set on the `<Form>` component:
4547

46-
* [`defaultValues`](#defaultvalues)
47-
* [`id`](#id)
48-
* [`noValidate`](#novalidate)
49-
* [`onSubmit`](#onsubmit)
50-
* [`sanitizeEmptyValues`](#sanitizeemptyvalues)
51-
* [`validate`](#validate)
52-
* [`warnWhenUnsavedChanges`](#warnwhenunsavedchanges)
48+
| Prop | Required | Type | Default | Description |
49+
| ------------------------ | -------- | ----------------- | ------- | ---------------------------------------------------------- |
50+
| `defaultValues` | Optional | `object|function` | - | The default values of the record. |
51+
| `id` | Optional | `string` | - | The id of the underlying `<form>` tag. |
52+
| `noValidate` | Optional | `boolean` | - | Set to `true` to disable the browser's default validation. |
53+
| `onSubmit` | Optional | `function` | `save` | A callback to call when the form is submitted. |
54+
| `sanitizeEmptyValues` | Optional | `boolean` | - | Set to `true` to remove empty values from the form state. |
55+
| `validate` | Optional | `function` | - | A function to validate the form values. |
56+
| `warnWhenUnsavedChanges` | Optional | `boolean` | - | Set to `true` to warn the user when leaving the form with unsaved changes. |
5357

5458
Additional props are passed to [the `useForm` hook](https://react-hook-form.com/api/useform).
5559

56-
**Reminder:** [react-hook-form's `formState` is wrapped with a Proxy](https://react-hook-form.com/api/useformstate/#rules) to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the `formState` before render in order to enable the subscription.
57-
58-
```js
59-
const { isDirty } = useFormState(); //
60-
const formState = useFormState(); // ❌ should deconstruct the formState
61-
```
62-
6360
## `defaultValues`
6461

6562
The value of the form `defaultValues` prop is an object, or a function returning an object, specifying default values for the created record. For instance:
@@ -237,3 +234,55 @@ export const TagEdit = () => (
237234
```
238235

239236
**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
237+
238+
## Subscribing To Form Changes
239+
240+
`<Form>` relies on [react-hook-form's `useForm`](https://react-hook-form.com/docs/useform) to manage the form state and validation. You can subscribe to form changes using the [`useFormContext`](https://react-hook-form.com/docs/useformcontext) and [`useFormState`](https://react-hook-form.com/docs/useformstate) hooks.
241+
242+
**Reminder:** [react-hook-form's `formState` is wrapped with a Proxy](https://react-hook-form.com/api/useformstate/#rules) to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the `formState` before render in order to enable the subscription.
243+
244+
```js
245+
const { isDirty } = useFormState(); //
246+
const formState = useFormState(); // ❌ should deconstruct the formState
247+
```
248+
249+
## AutoSave
250+
251+
In forms where users may spend a lot of time, it's a good idea to save the form automatically after a few seconds of inactivity. You can auto save the form content by using [the `<AutoSave>` component](./AutoSave.md).
252+
253+
<video controls autoplay playsinline muted loop>
254+
<source src="./img/AutoSave.webm" type="video/webm"/>
255+
<source src="./img/AutoSave.mp4" type="video/mp4"/>
256+
Your browser does not support the video tag.
257+
</video>
258+
259+
{% raw %}
260+
```tsx
261+
import { AutoSave } from '@react-admin/ra-form-layout';
262+
import { Edit, Form, TextInput, DateInput, SelectInput } from 'react-admin';
263+
import { Stack } from '@mui/material';
264+
265+
const PersonEdit = () => (
266+
<Edit mutationMode="optimistic">
267+
<Form resetOptions={{ keepDirtyValues: true }}>
268+
<Stack>
269+
<TextInput source="first_name" />
270+
<TextInput source="last_name" />
271+
<DateInput source="dob" />
272+
<SelectInput source="sex" choices={[
273+
{ id: 'male', name: 'Male' },
274+
{ id: 'female', name: 'Female' },
275+
]}/>
276+
</Stack>
277+
<AutoSave />
278+
</Form>
279+
</Edit>
280+
);
281+
```
282+
{% endraw %}
283+
284+
Note that you **must** set the `<Form resetOptions>` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.
285+
286+
If you're using it in an `<Edit>` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `<AutoSave>` doesn't work with the default `mutationMode="undoable"`.
287+
288+
Check [the `<AutoSave>` component](./AutoSave.md) documentation for more details.

‎docs/LongForm.md

+273-22
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,181 @@ const CustomerEdit = () => (
8282
);
8383
```
8484

85-
`<LongForm>` accepts the same props as [the `<Form>` component](./Form.md).
85+
## Props
86+
87+
Here are all the props you can set on the `<LongForm>` component:
88+
89+
| Prop | Required | Type | Default | Description |
90+
| ------------------------ | -------- | ----------------- | ------- | ---------------------------------------------------------- |
91+
| `children` | Required | `ReactNode` | - | A list of `<LongForm.Section>` elements. |
92+
| `defaultValues` | Optional | `object|function` | - | The default values of the record. |
93+
| `id` | Optional | `string` | - | The id of the underlying `<form>` tag. |
94+
| `noValidate` | Optional | `boolean` | - | Set to `true` to disable the browser's default validation. |
95+
| `onSubmit` | Optional | `function` | `save` | A callback to call when the form is submitted. |
96+
| `sanitizeEmptyValues` | Optional | `boolean` | - | Set to `true` to remove empty values from the form state. |
97+
| `sx` | Optional | `object` | - | An object containing the Material UI style overrides to apply to the root component |
98+
| `toolbar` | Optional | `ReactElement` | - | A custom toolbar element. |
99+
| `validate` | Optional | `function` | - | A function to validate the form values. |
100+
| `warnWhenUnsavedChanges` | Optional | `boolean` | - | Set to `true` to warn the user when leaving the form with unsaved changes. |
86101

87-
* `defaultValues`
88-
* `id`
89-
* `noValidate`
90-
* `onSubmit`
91-
* `sx`
92-
* `toolbar`
93-
* `validate`
94-
* `warnWhenUnsavedChanges`
95102

96103
Additional props are passed to `react-hook-form`'s [`useForm` hook](https://react-hook-form.com/api/useform).
97104

105+
## `children`
106+
107+
The children of `<LongForm>` must be [`<LongForm.Section>` elements](#longformsection).
108+
109+
```jsx
110+
const CustomerEdit = () => (
111+
<Edit component="div">
112+
<LongForm>
113+
<LongForm.Section label="Identity">
114+
...
115+
</LongForm.Section>
116+
<LongForm.Section label="Occupations">
117+
...
118+
</LongForm.Section>
119+
<LongForm.Section label="Preferences">
120+
...
121+
</LongForm.Section>
122+
</LongForm>
123+
</Edit>
124+
);
125+
```
126+
127+
## `defaultValues`
128+
129+
The value of the form `defaultValues` prop is an object, or a function returning an object, specifying default values for the created record. For instance:
130+
131+
```jsx
132+
const postDefaultValue = () => ({ id: uuid(), created_at: new Date(), nb_views: 0 });
133+
134+
export const PostCreate = () => (
135+
<Create>
136+
<LongForm defaultValues={postDefaultValue}>
137+
<LongForm.Section label="Summary">
138+
<TextInput source="title" />
139+
<RichTextInput source="body" />
140+
<NumberInput source="nb_views" />
141+
<SaveButton />
142+
</LongForm.Section>
143+
</Form>
144+
</Create>
145+
);
146+
```
147+
148+
**Tip**: You can include properties in the form `defaultValues` that are not listed as input components, like the `created_at` property in the previous example.
149+
150+
**Tip**: React-admin also allows to define default values at the input level. See the [Setting default Values](./EditTutorial.md#setting-default-values) section.
151+
152+
## `id`
153+
154+
Normally, a submit button only works when placed inside a `<form>` tag. However, you can place a submit button outside the form if the submit button `form` matches the form `id`.
155+
156+
Set this form `id` via the `id` prop.
157+
158+
```jsx
159+
export const PostCreate = () => (
160+
<Create>
161+
<LongForm id="post_create_form">
162+
<LongForm.Section label="summary">
163+
<TextInput source="title" />
164+
<RichTextInput source="body" />
165+
<NumberInput source="nb_views" />
166+
</LongForm.Section>
167+
</LongForm>
168+
<SaveButton form="post_create_form" />
169+
</Create>
170+
);
171+
```
172+
173+
## `noValidate`
174+
175+
The `<form novalidate>` attribute prevents the browser from validating the form. This is useful if you don't want to use the browser's default validation, or if you want to customize the error messages. To set this attribute on the underlying `<form>` tag, set the `noValidate` prop to `true`.
176+
177+
```jsx
178+
const PostCreate = () => (
179+
<Create>
180+
<LongForm noValidate>
181+
...
182+
</LongForm>
183+
</Create>
184+
);
185+
```
186+
187+
## `onSubmit`
188+
189+
By default, `<LongForm>` calls the `save` callback passed to it by the edit or create controller, via the `SaveContext`. You can override this behavior by setting a callback as the `onSubmit` prop manually.
190+
191+
```jsx
192+
export const PostCreate = () => {
193+
const [create] = useCreate();
194+
const postSave = (data) => {
195+
create('posts', { data });
196+
};
197+
return (
198+
<Create>
199+
<LongForm onSubmit={postSave}>
200+
...
201+
</LongForm>
202+
</Create>
203+
);
204+
};
205+
```
206+
207+
## `sanitizeEmptyValues`
208+
209+
In HTML, the value of empty form inputs is the empty string (`''`). React-admin inputs (like `<TextInput>`, `<NumberInput>`, etc.) automatically transform these empty values into `null`.
210+
211+
But for your own input components based on react-hook-form, this is not the default. React-hook-form doesn't transform empty values by default. This leads to unexpected `create` and `update` payloads like:
212+
213+
```jsx
214+
{
215+
id: 1234,
216+
title: 'Lorem Ipsum',
217+
is_published: '',
218+
body: '',
219+
// etc.
220+
}
221+
```
222+
223+
If you prefer to omit the keys for empty values, set the `sanitizeEmptyValues` prop to `true`. This will sanitize the form data before passing it to the `dataProvider`, i.e. remove empty strings from the form state, unless the record actually had a value for that field before edition.
224+
225+
```jsx
226+
const PostCreate = () => (
227+
<Create>
228+
<LongForm sanitizeEmptyValues>
229+
...
230+
</LongForm>
231+
</Create>
232+
);
233+
```
234+
235+
For the previous example, the data sent to the `dataProvider` will be:
236+
237+
```jsx
238+
{
239+
id: 1234,
240+
title: 'Lorem Ipsum',
241+
}
242+
```
243+
244+
**Note:** Setting the `sanitizeEmptyValues` prop to `true` will also have a (minor) impact on react-admin inputs (like `<TextInput>`, `<NumberInput>`, etc.): empty values (i.e. values equal to `null`) will be removed from the form state on submit, unless the record actually had a value for that field.
245+
246+
If you need a more fine-grained control over the sanitization, you can use [the `transform` prop](./Edit.md#transform) of `<Edit>` or `<Create>` components, or [the `parse` prop](./Inputs.md#parse) of individual inputs.
247+
248+
## `sx`: CSS API
249+
250+
The `<LongForm>` component accepts the usual `className` prop. You can also override the styles of the inner components thanks to the `sx` property. This property accepts the following subclasses:
251+
252+
| Rule name | Description |
253+
|------------------------|----------------------------------------|
254+
| `RaLongForm` | Applied to the root component |
255+
| `& .RaLongForm-toc` | Applied to the TOC |
256+
| `& .RaLongForm-main` | Applied to the main `<Card>` component |
257+
| `& .RaLongForm-toolbar`| Applied to the toolbar |
258+
| `& .RaLongForm-error` | Applied to the `<MenuItem>` in case the section has validation errors |
259+
98260
## `toolbar`
99261

100262
You can customize the form Toolbar by passing a custom element in the `toolbar` prop. The form expects the same type of element as `<SimpleForm>`, see [the `<SimpleForm toolbar>` prop documentation](https://marmelab.com/react-admin/CreateEdit.html#toolbar) in the react-admin docs.
@@ -107,15 +269,15 @@ import {
107269
} from 'react-admin';
108270
import { LongForm } from '@react-admin/ra-form-layout';
109271

110-
const CustomerCustomToolbar = props => (
272+
const CustomToolbar = props => (
111273
<RaToolbar {...props}>
112274
<SaveButton label="Save and return" type="button" variant="outlined" />
113275
</RaToolbar>
114276
);
115277

116-
const CustomerEditWithToolbar = () => (
278+
const CustomerEdit = () => (
117279
<Edit component="div">
118-
<LongForm toolbar={<CustomerCustomToolbar />}>
280+
<LongForm toolbar={<CustomToolbar />}>
119281
<LongForm.Section label="Identity">
120282
...
121283
</LongForm.Section>
@@ -130,17 +292,64 @@ const CustomerEditWithToolbar = () => (
130292
);
131293
```
132294

133-
## `sx`: CSS API
295+
## `validate`
134296

135-
The `<LongForm>` component accepts the usual `className` prop. You can also override the styles of the inner components thanks to the `sx` property. This property accepts the following subclasses:
297+
The value of the form `validate` prop must be a function taking the record as input, and returning an object with error messages indexed by field. For instance:
136298

137-
| Rule name | Description |
138-
|------------------------|----------------------------------------|
139-
| `RaLongForm` | Applied to the root component |
140-
| `& .RaLongForm-toc` | Applied to the TOC |
141-
| `& .RaLongForm-main` | Applied to the main `<Card>` component |
142-
| `& .RaLongForm-toolbar`| Applied to the toolbar |
143-
| `& .RaLongForm-error` | Applied to the `<MenuItem>` in case the section has validation errors |
299+
```jsx
300+
const validateUserCreation = (values) => {
301+
const errors = {};
302+
if (!values.firstName) {
303+
errors.firstName = 'The firstName is required';
304+
}
305+
if (!values.age) {
306+
// You can return translation keys
307+
errors.age = 'ra.validation.required';
308+
} else if (values.age < 18) {
309+
// Or an object if the translation messages need parameters
310+
errors.age = {
311+
message: 'ra.validation.minValue',
312+
args: { min: 18 }
313+
};
314+
}
315+
return errors
316+
};
317+
318+
export const UserCreate = () => (
319+
<Create>
320+
<LongForm validate={validateUserCreation}>
321+
<LongForm.Section label="Summary">
322+
<TextInput label="First Name" source="firstName" />
323+
<TextInput label="Age" source="age" />
324+
</LongForm.Section>
325+
</LongForm>
326+
</Create>
327+
);
328+
```
329+
330+
**Tip**: React-admin also allows to define validation rules at the input level. See [the Validation chapter](./Validation.md#per-input-validation-built-in-field-validators) for details.
331+
332+
**Tip**: The `validate` function can return a promise for asynchronous validation. See [the Server-Side Validation section](./Validation.md#server-side-validation) in the Validation documentation.
333+
334+
## `warnWhenUnsavedChanges`
335+
336+
React-admin keeps track of the form state, so it can detect when the user leaves an `Edit` or `Create` page with unsaved changes. To avoid data loss, you can use this ability to ask the user to confirm before leaving a page with unsaved changes.
337+
338+
![Warn About Unsaved Changes](./img/warn_when_unsaved_changes.png)
339+
340+
Warning about unsaved changes is an opt-in feature: you must set the `warnWhenUnsavedChanges` prop in the form component to enable it:
341+
342+
```jsx
343+
export const TagEdit = () => (
344+
<Edit>
345+
<LongForm warnWhenUnsavedChanges>
346+
...
347+
</LongForm>
348+
</Edit>
349+
);
350+
```
351+
352+
**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
144353

145354
## `<LongForm.Section>`
146355

@@ -215,4 +424,46 @@ const CustomerEditWithCardinality = () => {
215424
</Edit>
216425
);
217426
};
218-
```
427+
```
428+
429+
## AutoSave
430+
431+
In forms where users may spend a lot of time, it's a good idea to save the form automatically after a few seconds of inactivity. You turn on this feature by using [the `<AutoSave>` component](./AutoSave.md).
432+
433+
{% raw %}
434+
```tsx
435+
import { LongForm, AutoSave } from '@react-admin/ra-form-layout';
436+
import { Edit, TextInput, DateInput, SelectInput, Toolbar } from 'react-admin';
437+
438+
const AutoSaveToolbar = () => (
439+
<Toolbar>
440+
<AutoSave />
441+
</Toolbar>
442+
);
443+
444+
const PersonEdit = () => (
445+
<Edit mutationMode="optimistic">
446+
<LongForm
447+
resetOptions={{ keepDirtyValues: true }}
448+
toolbar={AutoSaveToolbar}
449+
>
450+
<LongForm.Section label="identity">
451+
<TextInput source="first_name" />
452+
<TextInput source="last_name" />
453+
<DateInput source="dob" />
454+
<SelectInput source="sex" choices={[
455+
{ id: 'male', name: 'Male' },
456+
{ id: 'female', name: 'Female' },
457+
]}/>
458+
</LongForm.Section>
459+
</LongForm>
460+
</Edit>
461+
);
462+
```
463+
{% endraw %}
464+
465+
Note that you **must** set the `<LongForm resetOptions>` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.
466+
467+
If you're using it in an `<Edit>` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `<AutoSave>` doesn't work with the default `mutationMode="undoable"`.
468+
469+
Check [the `<AutoSave>` component](./AutoSave.md) documentation for more details.

‎docs/Reference.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ title: "Index"
1818
* [`<Authenticated>`](./Authenticated.md)
1919
* [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md)
2020
* [`<AutocompleteInput>`](./AutocompleteInput.md)
21+
* [`<AutoSave>`](./AutoSave.md)<img class="icon" src="./img/premium.svg" />
2122

2223
**- B -**
2324
* [`<Breadcrumb>`](./Breadcrumb.md)<img class="icon" src="./img/premium.svg" />
@@ -95,16 +96,16 @@ title: "Index"
9596
**- L -**
9697
* [`<Labeled>`](./Labeled.md)
9798
* [`<Layout>`](./Theming.md#using-a-custom-layout)
98-
* [`<Loading>`](./Theming.md#loading)
9999
* [`<LinearProgress>`](./Theming.md#linearprogress)
100-
* [`<LocalesMenuButton>`](./LocalesMenuButton.md)
101-
* [`<Logout>`](./Theming.md#using-a-custom-logout-button)
102100
* [`<List>`](./List.md#usage)
103101
* [`<ListBase>`](./ListBase.md#usage)
104-
* [`<ListGuesser>`](./ListGuesser.md#usage)
105102
* [`<ListButton>`](./Buttons.md#listbutton)
103+
* [`<ListGuesser>`](./ListGuesser.md#usage)
106104
* [`<ListLive>`](./ListLive.md)<img class="icon" src="./img/premium.svg" />
105+
* [`<Loading>`](./Theming.md#loading)
107106
* [`<LocalesMenuButton>`](./LocalesMenuButton.md)
107+
* [`<LongForm>`](./LongForm.md)<img class="icon" src="./img/premium.svg" />
108+
* [`<Logout>`](./Theming.md#using-a-custom-logout-button)
108109

109110
**- M -**
110111
* [`<MarkdownField>`](./MarkdownField.md)<img class="icon" src="./img/premium.svg" />

‎docs/SimpleForm.md

+100-15
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,46 @@ export const PostCreate = () => (
3131

3232
`<SimpleForm>` calls react-hook-form's `useForm` hook, and places the result in a `FormProvider` component. This means you can take advantage of the [`useFormContext`](https://react-hook-form.com/api/useformcontext) and [`useFormState`](https://react-hook-form.com/api/useformstate) hooks to access the form state.
3333

34+
## Props
35+
3436
Here are all the props you can set on the `<SimpleForm>` component:
3537

36-
* [`component`](#component)
37-
* [`defaultValues`](#defaultvalues)
38-
* [`id`](#id)
39-
* [`noValidate`](#novalidate)
40-
* [`onSubmit`](#onsubmit)
41-
* [`sanitizeEmptyValues`](#sanitizeemptyvalues)
42-
* [`sx`](#sx-css-api)
43-
* [`toolbar`](#toolbar)
44-
* [`validate`](#validate)
45-
* [`warnWhenUnsavedChanges`](#warnwhenunsavedchanges)
38+
| Prop | Required | Type | Default | Description |
39+
| ------------------------- | -------- | ------------------ | ------- | ---------------------------------------------------------- |
40+
| `children` | Required | `element` | - | The form content. |
41+
| `component` | Optional | `elementType` | `CardContent` | The component used to wrap the form. |
42+
| `defaultValues` | Optional | `object| function` | - | The default values of the record. |
43+
| `id` | Optional | `string` | - | The id of the underlying `<form>` tag. |
44+
| `noValidate` | Optional | `boolean` | - | Set to `true` to disable the browser's default validation. |
45+
| `onSubmit` | Optional | `function` | `save` | A callback to call when the form is submitted. |
46+
| `sanitize EmptyValues` | Optional | `boolean` | - | Set to `true` to remove empty values from the form state. |
47+
| `sx` | Optional | `object` | - | Custom styles |
48+
| `toolbar` | Optional | `element` | - | The toolbar component. |
49+
| `validate` | Optional | `function` | - | A function to validate the form values. |
50+
| `warnWhen UnsavedChanges` | Optional | `boolean` | - | Set to `true` to warn the user when leaving the form with unsaved changes. |
4651

47-
Additional props are passed to [the `useForm` hook](https://react-hook-form.com/api/useform).
52+
Additional props are passed to [the `useForm` hook](https://react-hook-form.com/api/useform) and to [the material-ui `<Stack>` component](https://mui.com/material-ui/react-stack/).
4853

49-
**Reminder:** [react-hook-form's `formState` is wrapped with a Proxy](https://react-hook-form.com/api/useformstate/#rules) to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the `formState` before render in order to enable the subscription.
54+
## `children`
5055

51-
```js
52-
const { isDirty } = useFormState(); //
53-
const formState = useFormState(); // ❌ should deconstruct the formState
56+
`<SimpleForm>` renders its children (usually Input components) row by row. It uses a [Material UI `<Stack>`](https://mui.com/material-ui/react-stack/).
57+
58+
```jsx
59+
import { Create, SimpleForm, TextInput, RichTextInput, NumberInput } from 'react-admin';
60+
61+
export const PostCreate = () => (
62+
<Create>
63+
<SimpleForm>
64+
<TextInput source="title" />
65+
<RichTextInput source="body" />
66+
<NumberInput source="nb_views" />
67+
</SimpleForm>
68+
</Create>
69+
);
5470
```
5571

72+
You can also pass non-input children to build a custom form layout. See the [Complex Input Layout](#complex-input-layout) section for an example.
73+
5674
## `component`
5775

5876
`<SimpleForm>` renders a Material UI `<CardContent>` by default. You replace it by any component you want as wrapper, just pass it as the `component` prop.
@@ -477,6 +495,27 @@ const Separator = () => <Box pt="1em" />;
477495
```
478496
{% endraw %}
479497

498+
Before building your own custom layout, take a look at the existing form layout components provided by react-admin:
499+
500+
- [`SimpleForm`](./SimpleForm.md) for a single-column layout
501+
- [`TabbedForm`](./TabbedForm.md) for a tabbed layout
502+
- [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections
503+
- [`LongForm`](./LongForm.md) for long forms with a navigation sidebar
504+
- [`WizardForm`](./WizardForm.md) for multi-step forms
505+
- [`EditInDialog`](./EditInDialog.md) for sub-forms in a modal dialog
506+
- and [`Form`](./Form.md), a headless component to use as a base for your custom layouts
507+
508+
## Subscribing To Form Changes
509+
510+
`<SimpleForm>` relies on [react-hook-form's `useForm`](https://react-hook-form.com/docs/useform) to manage the form state and validation. You can subscribe to form changes using the [`useFormContext`](https://react-hook-form.com/docs/useformcontext) and [`useFormState`](https://react-hook-form.com/docs/useformstate) hooks.
511+
512+
**Reminder:** [react-hook-form's `formState` is wrapped with a Proxy](https://react-hook-form.com/api/useformstate/#rules) to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the `formState` before render in order to enable the subscription.
513+
514+
```js
515+
const { isDirty } = useFormState(); //
516+
const formState = useFormState(); // ❌ should deconstruct the formState
517+
```
518+
480519
## Displaying Inputs Based On Permissions
481520

482521
You can leverage [the `usePermissions` hook](./usePermissions.md) to display inputs if the user has the required permissions.
@@ -565,3 +604,49 @@ const PostEdit = () => (
565604
```
566605

567606
`<SimpleFormConfigurable>` accepts the same props as `<SimpleForm>`.
607+
608+
## AutoSave
609+
610+
In forms where users may spend a lot of time, it's a good idea to save the form automatically after a few seconds of inactivity. You can auto save the form content by using [the `<AutoSave>` component](./AutoSave.md).
611+
612+
<video controls autoplay playsinline muted loop>
613+
<source src="./img/AutoSave.webm" type="video/webm"/>
614+
<source src="./img/AutoSave.mp4" type="video/mp4"/>
615+
Your browser does not support the video tag.
616+
</video>
617+
618+
{% raw %}
619+
```tsx
620+
import { AutoSave } from '@react-admin/ra-form-layout';
621+
import { Edit, SimpleForm, TextInput, DateInput, SelectInput, Toolbar } from 'react-admin';
622+
623+
const AutoSaveToolbar = () => (
624+
<Toolbar>
625+
<AutoSave />
626+
</Toolbar>
627+
);
628+
629+
const PersonEdit = () => (
630+
<Edit mutationMode="optimistic">
631+
<SimpleForm
632+
resetOptions={{ keepDirtyValues: true }}
633+
toolbar={AutoSaveToolbar}
634+
>
635+
<TextInput source="first_name" />
636+
<TextInput source="last_name" />
637+
<DateInput source="dob" />
638+
<SelectInput source="sex" choices={[
639+
{ id: 'male', name: 'Male' },
640+
{ id: 'female', name: 'Female' },
641+
]}/>
642+
</SimpleForm>
643+
</Edit>
644+
);
645+
```
646+
{% endraw %}
647+
648+
Note that you **must** set the `<SimpleForm resetOptions>` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.
649+
650+
If you're using it in an `<Edit>` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `<AutoSave>` doesn't work with the default `mutationMode="undoable"`.
651+
652+
Check [the `<AutoSave>` component](./AutoSave.md) documentation for more details.

‎docs/TabbedForm.md

+87-16
Original file line numberDiff line numberDiff line change
@@ -74,27 +74,49 @@ export const PostEdit = () => (
7474

7575
React-admin highlights the tabs containing validation errors to help users locate incorrect input values.
7676

77+
## Props
78+
7779
Here are all the props you can set on the `<TabbedForm>` component:
7880

79-
* [`component`](#component)
80-
* [`defaultValues`](#defaultvalues)
81-
* [`id`](#id)
82-
* [`noValidate`](#novalidate)
83-
* [`onSubmit`](#onsubmit)
84-
* [`sx`](#sx-css-api)
85-
* [`syncWithLocation`](#syncwithlocation)
86-
* [`tabs`](#tabs)
87-
* [`toolbar`](#toolbar)
88-
* [`validate`](#validate)
89-
* [`warnWhenUnsavedChanges`](#warnwhenunsavedchanges)
81+
| Prop | Required | Type | Default | Description |
82+
| ------------------------- | -------- | ------------------ | ------- | ---------------------------------------------------------- |
83+
| `children` | Required | `element` | - | The form content. |
84+
| `component` | Optional | `elementType` | `CardContent` | The component used to wrap the form. |
85+
| `defaultValues` | Optional | `object| function` | - | The default values of the record. |
86+
| `id` | Optional | `string` | - | The id of the underlying `<form>` tag. |
87+
| `noValidate` | Optional | `boolean` | - | Set to `true` to disable the browser's default validation. |
88+
| `onSubmit` | Optional | `function` | `save` | A callback to call when the form is submitted. |
89+
| `sanitize EmptyValues` | Optional | `boolean` | - | Set to `true` to remove empty values from the form state. |
90+
| `sx` | Optional | `object` | - | Custom styles |
91+
| `toolbar` | Optional | `element` | - | The toolbar component. |
92+
| `validate` | Optional | `function` | - | A function to validate the form values. |
93+
| `warnWhen UnsavedChanges` | Optional | `boolean` | - | Set to `true` to warn the user when leaving the form with unsaved changes. |
9094

91-
Additional props are passed to [the `useForm` hook](https://react-hook-form.com/api/useform).
95+
Additional props are passed to [the `useForm` hook](https://react-hook-form.com/api/useform) and to the wrapper `<div>` component.
9296

93-
**Reminder:** [react-hook-form's `formState` is wrapped with a Proxy](https://react-hook-form.com/api/useformstate/#rules) to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the `formState` before render in order to enable the subscription.
97+
## `children`
9498

95-
```js
96-
const { isDirty } = useFormState(); //
97-
const formState = useFormState(); // ❌ should deconstruct the formState
99+
`<TabbedForm>` expects `<TabbedForm.Tab>` elements as children. It renders them as tabs using [a Material UI `<Tabs>` component](https://mui.com/material-ui/react-tabs/).
100+
101+
```jsx
102+
export const PostEdit = () => (
103+
<Edit>
104+
<TabbedForm>
105+
<TabbedForm.Tab label="summary">
106+
...
107+
</TabbedForm.Tab>
108+
<TabbedForm.Tab label="body">
109+
...
110+
</TabbedForm.Tab>
111+
<TabbedForm.Tab label="Miscellaneous">
112+
...
113+
</TabbedForm.Tab>
114+
<TabbedForm.Tab label="comments">
115+
...
116+
</TabbedForm.Tab>
117+
</TabbedForm>
118+
</Edit>
119+
);
98120
```
99121

100122
## `component`
@@ -643,6 +665,17 @@ const ProductEditDetails = () => (
643665
```
644666
{% endraw %}
645667

668+
## Subscribing To Form Changes
669+
670+
`<TabbedForm>` relies on [react-hook-form's `useForm`](https://react-hook-form.com/docs/useform) to manage the form state and validation. You can subscribe to form changes using the [`useFormContext`](https://react-hook-form.com/docs/useformcontext) and [`useFormState`](https://react-hook-form.com/docs/useformstate) hooks.
671+
672+
**Reminder:** [react-hook-form's `formState` is wrapped with a Proxy](https://react-hook-form.com/api/useformstate/#rules) to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the `formState` before render in order to enable the subscription.
673+
674+
```js
675+
const { isDirty } = useFormState(); //
676+
const formState = useFormState(); // ❌ should deconstruct the formState
677+
```
678+
646679
## Dynamic Tab Label
647680

648681
`<TabbedForm>` often contain not only inputs, but also related data (e.g. the reviews of a product). Users appreviate that the label of such tabs show the actual number of related elements, to avoid clicking on a tab to reveal an empty list.
@@ -750,3 +783,41 @@ const UserEdit = () => {
750783
};
751784
```
752785
{% endraw %}
786+
787+
## AutoSave
788+
789+
In forms where users may spend a lot of time, it's a good idea to save the form automatically after a few seconds of inactivity. You can auto save the form content by using [the `<AutoSave>` component](./AutoSave.md).
790+
791+
{% raw %}
792+
```tsx
793+
import { AutoSave } from '@react-admin/ra-form-layout';
794+
import { Edit, SaveButton, TabbedForm, TextInput, Toolbar } from 'react-admin';
795+
796+
const AutoSaveToolbar = () => (
797+
<Toolbar>
798+
<SaveButton />
799+
<AutoSave />
800+
</Toolbar>
801+
);
802+
803+
const PostEdit = () => (
804+
<Edit mutationMode="optimistic">
805+
<TabbedForm
806+
resetOptions={{ keepDirtyValues: true }}
807+
toolbar={AutoSaveToolbar}
808+
>
809+
<TabbedForm.Tab label="summary">
810+
<TextInput source="title" />
811+
<TextInput source="teaser" />
812+
</TabbedForm.Tab>
813+
</TabbedForm>
814+
</Edit>
815+
);
816+
```
817+
{% endraw %}
818+
819+
Note that you **must** set the `<TabbedForm resetOptions>` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.
820+
821+
If you're using it in an `<Edit>` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `<AutoSave>` doesn't work with the default `mutationMode="undoable"`.
822+
823+
Check [the `<AutoSave>` component](./AutoSave.md) documentation for more details.

‎docs/Validation.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ title: "Form Validation"
55

66
# Form Validation
77

8+
![Validation example](./img/validation.png)
9+
810
React-admin relies on [react-hook-form](https://react-hook-form.com/) for the validation of user input in forms. React-admin supports several approaches:
911

1012
- using the `validate` prop at the Form level (validation by function)

‎docs/WizardForm.md

+418-8
Large diffs are not rendered by default.

‎docs/img/AutoSave.mp4

115 KB
Binary file not shown.

‎docs/img/AutoSave.webm

155 KB
Binary file not shown.

‎docs/img/validation.png

181 KB
Loading

‎docs/navigation.html

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
<li {% if page.path == 'JsonSchemaForm.md' %} class="active" {% endif %}><a class="nav-link" href="./JsonSchemaForm.html"><code>&lt;JsonSchemaForm&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
114114
<li {% if page.path == 'Toolbar.md' %} class="active" {% endif %}><a class="nav-link" href="./Toolbar.html"><code>&lt;Toolbar&gt;</code></a></li>
115115
<li {% if page.path == 'SaveButton.md' %} class="active" {% endif %}><a class="nav-link" href="./SaveButton.html"><code>&lt;SaveButton&gt;</code></a></li>
116+
<li {% if page.path == 'AutoSave.md' %} class="active" {% endif %}><a class="nav-link" href="./AutoSave.html"><code>&lt;AutoSave&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
116117
<li {% if page.path == 'useCreateContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useCreateContext.html"><code>useCreateContext</code></a></li>
117118
<li {% if page.path == 'useCreateController.md' %} class="active" {% endif %}><a class="nav-link" href="./useCreateController.html"><code>useCreateController</code></a></li>
118119
<li {% if page.path == 'useEditContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useEditContext.html"><code>useEditContext</code></a></li>

0 commit comments

Comments
 (0)
Please sign in to comment.