Skip to content

Commit 7a1c2b8

Browse files
authored
Merge pull request #10690 from marmelab/In-Place-Editor
Add `<InPlaceEditor>` for edit-in-place
2 parents baecacd + 97438e1 commit 7a1c2b8

17 files changed

+1542
-1
lines changed

docs/Forms.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,3 +770,39 @@ Users often need to edit data from several resources in the same form. React-adm
770770
<source src="./img/reference-many-input.mp4" type="video/mp4"/>
771771
Your browser does not support the video tag.
772772
</video>
773+
774+
## Edit In Place
775+
776+
Instead of asking users to fill a form to edit a record, you can let them edit the record straight from the list or show view. [The `<InPlaceEditor>` component](./InPlaceEditor.md) uses a `<TextField>` in read mode, and a `<TextInput>` in edition mode. It is useful for quick edits without navigating to a separate edit page.
777+
778+
<video controls autoplay playsinline muted loop>
779+
<source src="./img/InPlaceEditor.mp4" type="video/mp4"/>
780+
Your browser does not support the video tag.
781+
</video>
782+
783+
{% raw %}
784+
```tsx
785+
import { Show, InPlaceEditor } from 'react-admin';
786+
import { Stack, Box, Typography } from '@mui/material';
787+
788+
const CustomerShow = () => (
789+
<Show>
790+
<Stack direction="row" spacing={2}>
791+
<AvatarField />
792+
<CustomerActions />
793+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
794+
<Typography>Phone</Typography>
795+
<InPlaceEditor source="phone" />
796+
</Box>
797+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
798+
<Typography>Email</Typography>
799+
<InPlaceEditor source="email" />
800+
</Box>
801+
...
802+
</Stack>
803+
</Show>
804+
);
805+
```
806+
{% endraw %}
807+
808+
Check out [the `<InPlaceEditor>` documentation](./InPlaceEditor.md) for more details.

docs/InPlaceEditor.md

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
layout: default
3+
title: "The InPlaceEditor Component"
4+
---
5+
6+
# `<InPlaceEditor>`
7+
8+
`<InPlaceEditor>` renders a field from the current record. On click, it switches to an editable state, allowing the user to change the value directly.
9+
10+
<video controls autoplay playsinline muted loop>
11+
<source src="./img/InPlaceEditor.mp4" type="video/mp4"/>
12+
Your browser does not support the video tag.
13+
</video>
14+
15+
Use this component to let users edit parts of a record directly in the list or detail view. It is useful for quick edits without navigating to a separate edit page.
16+
17+
The field changes color on hover, to indicate that it is editable. The user can cancel the edit by pressing Escape. The field is saved automatically when the user clicks outside of it or presses Enter. While it is being saved, the field is disabled and a loading spinner is shown. If the save fails, an error message is displayed and the original value is restored.
18+
19+
## Usage
20+
21+
Use `<InPlaceEditor>` inside a `RecordContext` (e.g., under `<List>` or `<Show>`) and pass it a `source` prop to specify which field to edit. The component will render the field with a `<TextField>` and let the user edit it with a `<TextInput>`
22+
23+
{% raw %}
24+
```tsx
25+
import { Show, InPlaceEditor } from 'react-admin';
26+
import { Stack, Box, Typography } from '@mui/material';
27+
import { AvatarField, CustomerActions } from './components';
28+
29+
const CustomerShow = () => (
30+
<Show>
31+
<Stack direction="row" spacing={2}>
32+
<AvatarField />
33+
<CustomerActions />
34+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
35+
<Typography>Phone</Typography>
36+
<InPlaceEditor source="phone" />
37+
</Box>
38+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
39+
<Typography>Email</Typography>
40+
<InPlaceEditor source="email" />
41+
</Box>
42+
...
43+
</Stack>
44+
</Show>
45+
);
46+
```
47+
{% endraw %}
48+
49+
**Note**: `<InPlaceEditor>` creates a `<Form>`, so it cannot be used inside an existing form (e.g., inside a `<SimpleForm>` or `<TabbedForm>`).
50+
51+
Instead of using the `source` prop, you can also specify the component to render in read mode with the `children` prop, and the component to render in edit mode with the `editor` prop. In general, you will need to tweak the styles of both components to make them look good together.
52+
53+
<video controls autoplay playsinline muted loop>
54+
<source src="./img/InPlaceEditorField.mp4" type="video/mp4"/>
55+
Your browser does not support the video tag.
56+
</video>
57+
58+
59+
{% raw %}
60+
```tsx
61+
const choices = [
62+
{ id: 'everyone', name: 'Everyone' },
63+
{ id: 'just_me', name: 'Just me' },
64+
{ id: 'sales', name: 'Sales' },
65+
];
66+
67+
// ...
68+
<InPlaceEditor
69+
editor={
70+
<SelectInput
71+
source="access"
72+
choices={choices}
73+
variant="standard"
74+
size="small"
75+
margin="none"
76+
label={false}
77+
helperText={false}
78+
autoFocus
79+
SelectProps={{ defaultOpen: true }}
80+
sx={{
81+
'& .MuiInput-root': { marginTop: 0 },
82+
'& .MuiSelect-select': { textAlign: 'right' },
83+
}}
84+
/>
85+
}
86+
>
87+
<SelectField
88+
source="access"
89+
variant="body1"
90+
choices={choices}
91+
sx={{ display: 'block', marginBottom: '5px' }}
92+
/>
93+
</InPlaceEditor>
94+
```
95+
{% endraw %}
96+
97+
## Props
98+
99+
| Prop | Required | Type | Default | Description |
100+
| ------------ | -------- | --------- | ------- | -------------------------------------------------------------------- |
101+
| `cancelOnBlur` | Optional | `boolean` | `false` | Whether to cancel the edit when the field loses focus. |
102+
| `children` | Optional | `ReactNode` | | The component to render in read mode. |
103+
| `editor` | Optional | `ReactNode` | | The component to render in edit mode. |
104+
| `mutationMode` | Optional | `string` | `pessimistic` | The mutation mode to use when saving the record. |
105+
| `mutationOptions` | Optional | `object` | | The options to pass to the `useUpdate` hook. |
106+
| `notifyOnSuccess` | Optional | `boolean` | `false` | Whether to show a notification on successful save. |
107+
| `resource` | Optional | `string` | | The name of the resource. |
108+
| `showButtons` | Optional | `boolean` | `false` | Whether to show the save and cancel buttons. |
109+
| `source` | Optional | `string` | | The name of the field to edit. |
110+
| `sx` | Optional | `SxProps` | | The styles to apply to the component. |
111+
112+
## `cancelOnBlur`
113+
114+
By default, when the user clicks outside of the field in edit mode, it saves the current value. If `cancelOnBlur` is set to true, the edit will be canceled instead and the initial value will be restored.
115+
116+
```tsx
117+
<InPlaceEditor source="phone" cancelOnBlur />
118+
```
119+
120+
## `children`
121+
122+
The component to render in read mode. By default, it's a `<TextField>` using the `source` prop.
123+
124+
You can use any [field component](./Fields.md) instead, as it renders in a `RecordContext`.
125+
126+
![InPlaceEditor children](./img/InPlaceEditorChildren.png)
127+
128+
For example, to render a `<SelectField>` in read mode, you can use the following code:
129+
130+
{% raw %}
131+
```tsx
132+
<InPlaceEditor source="leadStatus">
133+
<SelectField
134+
source="leadStatus"
135+
choices={[
136+
{ id: 'customer', name: 'Customer' },
137+
{ id: 'prospect', name: 'Prospect' },
138+
]}
139+
optionText={
140+
<ChipField
141+
size="small"
142+
variant="outlined"
143+
source="name"
144+
color="success"
145+
/>
146+
}
147+
sx={{
148+
display: 'block',
149+
marginBottom: '3px',
150+
marginTop: '2px',
151+
}}
152+
/>
153+
</InPlaceEditor>
154+
```
155+
{% endraw %}
156+
157+
## `editor`
158+
159+
The component to render in edit mode. By default, it's a `<TextInput>` using the `source` prop.
160+
161+
You can use any [input component](./Input.md) instead, as it renders in a `<Form>`. You will probably need to tweak the input variant, margin and style so that it matches the style of the read mode component.
162+
163+
<video controls autoplay playsinline muted loop>
164+
<source src="./img/InPlaceEditorField.mp4" type="video/mp4"/>
165+
Your browser does not support the video tag.
166+
</video>
167+
168+
For example, to use a `<SelectInput>` in edit mode, you can use the following code:
169+
170+
{% raw %}
171+
```tsx
172+
<InPlaceEditor
173+
editor={
174+
<SelectInput
175+
source="access"
176+
choices={choices}
177+
variant="standard"
178+
size="small"
179+
margin="none"
180+
label={false}
181+
helperText={false}
182+
autoFocus
183+
SelectProps={{ defaultOpen: true }}
184+
sx={{
185+
'& .MuiInput-root': { marginTop: 0 },
186+
'& .MuiSelect-select': { textAlign: 'right' },
187+
}}
188+
/>
189+
}
190+
>
191+
// ...
192+
</InPlaceEditor>
193+
```
194+
{% endraw %}
195+
196+
## `mutationMode`
197+
198+
The mutation mode to use when saving the record. By default, it is set to `pessimistic`, which means that the record is saved immediately when the user clicks outside of the field or presses Enter.
199+
200+
You can use any of the following values:
201+
202+
- `pessimistic`: On save, the field is dimmed to show the saving state. If the server returns an error, the UI is reverted to the previous state.
203+
- `optimistic`: The UI is updated immediately with the new value, without waiting for the server response. If the server returns an error, the UI is reverted to the previous state.
204+
- `undoable`: The record is saved immediately, but the user can undo the operation by clicking on the undo button in the notification. This must be used in conjunction with the `notifyOnSuccess` prop.
205+
206+
```tsx
207+
<InPlaceEditor source="phone" mutationMode="optimistic" />
208+
```
209+
210+
## `mutationOptions`
211+
212+
If you need to pass options to the `useUpdate` hook, you can use this prop.
213+
214+
This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.update()` call.
215+
216+
{% raw %}
217+
```tsx
218+
<InPlaceEditor
219+
source="phone"
220+
mutationOptions={{ meta: { foo: 'bar' } }}
221+
/>
222+
```
223+
{% endraw %}
224+
225+
## `notifyOnSuccess`
226+
227+
By default, the component does not show a notification when the record is saved. If you want to show a notification on successful save, set this prop to `true`.
228+
229+
![InPlaceEditor notifyOnSuccess](./img/InPlaceEditorNotifyOnSuccess.png)
230+
231+
```tsx
232+
<InPlaceEditor source="phone" notifyOnSuccess />
233+
```
234+
235+
## `resource`
236+
237+
The name of the resource. By default, it is set to the current resource in the `ResourceContext`. You can use this prop to override the resource name.
238+
239+
```tsx
240+
<InPlaceEditor source="phone" resource="customers" />
241+
```
242+
243+
## `showButtons`
244+
245+
By default, the component does not show the save and cancel buttons. If you want to show them, set this prop to `true`.
246+
247+
![InPlaceEditor showButtons](./img/InPlaceEditorShowButtons.png)
248+
249+
```tsx
250+
<InPlaceEditor source="phone" showButtons />
251+
```
252+
253+
## `source`
254+
255+
The name of the field to edit. You must set this prop, unless you define the `children` and `editor` props.
256+
257+
```tsx
258+
<InPlaceEditor source="phone" />
259+
```
260+
261+
## `sx`
262+
263+
The styles to apply to the component. Use it to alter the default styles of the reading, editing, and saving modes.
264+
265+
{% raw %}
266+
```tsx
267+
<InPlaceEditor
268+
source="phone"
269+
sx={{
270+
marginTop: '1rem',
271+
marginLeft: '1rem',
272+
'& .RaInPlaceEditor-reading div': {
273+
fontSize: '1.5rem',
274+
fontWeight: 'bold',
275+
color: 'primary.main',
276+
},
277+
'& .RaInPlaceEditor-editing input': {
278+
fontSize: '1.5rem',
279+
fontWeight: 'bold',
280+
color: 'primary.main',
281+
},
282+
'& .RaInPlaceEditor-saving div': {
283+
fontSize: '1.5rem',
284+
fontWeight: 'bold',
285+
color: 'text.disabled',
286+
},
287+
}}
288+
/>
289+
```
290+
{% endraw %}
291+
292+
You can use the `sx` prop to apply styles to the read mode, edit mode and saving mode. The following classes are available:
293+
294+
- `& .RaInPlaceEditor-reading`: The read mode.
295+
- `& .RaInPlaceEditor-editing`: The editing mode.
296+
- `& .RaInPlaceEditor-saving`: The saving mode.

docs/Reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ title: "Index"
106106
* [`<ImageField>`](./ImageField.md)
107107
* [`<ImageInput>`](./ImageInput.md)
108108
* [`<ImageInputPreview>`](./ImageInput.md#imageinput)
109+
* [`<InPlaceEditor>`](./InPlaceEditor.md)
109110
* [`<InspectorButton>`](./Configurable.md#inspectorbutton)
110111

111112
**- L -**

docs/TextField.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,43 @@ import { FunctionField } from 'react-admin';
5858
render={record => `${record.first_name} ${record.last_name}`}
5959
/>;
6060
```
61+
62+
## Edit In Place
63+
64+
In addition to rendering a field value, you may want to allow users to edit that value. You can redirect the user to an `<Edit>` page, or you can use the [`<InPlaceEditor>`](./InPlaceEditor.md) component to edit the value directly in the list or show view.
65+
66+
67+
68+
<video controls autoplay playsinline muted loop>
69+
<source src="./img/InPlaceEditor.mp4" type="video/mp4"/>
70+
Your browser does not support the video tag.
71+
</video>
72+
73+
`<InPlaceEditor>` renders a `<TextField>` by default, and turns into a `<TextInput>` when the user clicks on it. It is useful for quick edits without navigating to a separate edit page.
74+
75+
{% raw %}
76+
```tsx
77+
import { Show, InPlaceEditor } from 'react-admin';
78+
import { Stack, Box, Typography } from '@mui/material';
79+
80+
const CustomerShow = () => (
81+
<Show>
82+
<Stack direction="row" spacing={2}>
83+
<AvatarField />
84+
<CustomerActions />
85+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
86+
<Typography>Phone</Typography>
87+
<InPlaceEditor source="phone" />
88+
</Box>
89+
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
90+
<Typography>Email</Typography>
91+
<InPlaceEditor source="email" />
92+
</Box>
93+
...
94+
</Stack>
95+
</Show>
96+
);
97+
```
98+
{% endraw %}
99+
100+
Check out [the `<InPlaceEditor>` documentation](./InPlaceEditor.md) for more details.

0 commit comments

Comments
 (0)