Skip to content

Commit fd26e65

Browse files
authored
Merge pull request #10547 from marmelab/doc-useSimpleFormIteratorItem
[Doc] Document `useSourceContext` and improve `useSimpleFormIteratorItem` documentation
2 parents 518fbb6 + 6261fd9 commit fd26e65

File tree

10 files changed

+397
-48
lines changed

10 files changed

+397
-48
lines changed

docs/ArrayInput.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,54 @@ const OrderEdit = () => (
133133
</Edit>
134134
);
135135
```
136+
137+
## Changing An Item's Value Programmatically
138+
139+
You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.
140+
141+
However you need to know the `name` under which the input was registered in the form, and this name is dynamically generated depending on the index of the item in the array.
142+
143+
To get the name of the input for a given index, you can leverage the `SourceContext` created by react-admin, which can be accessed using the `useSourceContext` hook.
144+
145+
This context provides a `getSource` function that returns the effective `source` for an input in the current context, which you can use as input name for `setValue`.
146+
147+
Here is an example where we leverage `getSource` and `setValue` to change the role of an user to 'admin' when the 'Make Admin' button is clicked:
148+
149+
{% raw %}
150+
151+
```tsx
152+
import { ArrayInput, SimpleFormIterator, TextInput, useSourceContext } from 'react-admin';
153+
import { useFormContext } from 'react-hook-form';
154+
import { Button } from '@mui/material';
155+
156+
const MakeAdminButton = () => {
157+
const sourceContext = useSourceContext();
158+
const { setValue } = useFormContext();
159+
160+
const onClick = () => {
161+
// sourceContext.getSource('role') will for instance return
162+
// 'users.0.role'
163+
setValue(sourceContext.getSource('role'), 'admin');
164+
};
165+
166+
return (
167+
<Button onClick={onClick} size="small" sx={{ minWidth: 120 }}>
168+
Make admin
169+
</Button>
170+
);
171+
};
172+
173+
const UserArray = () => (
174+
<ArrayInput source="users">
175+
<SimpleFormIterator inline>
176+
<TextInput source="name" helperText={false} />
177+
<TextInput source="role" helperText={false} />
178+
<MakeAdminButton />
179+
</SimpleFormIterator>
180+
</ArrayInput>
181+
);
182+
```
183+
184+
{% endraw %}
185+
186+
**Tip:** If you only need the item's index, you can leverage the [`useSimpleFormIteratorItem` hook](./SimpleFormIterator.md#getting-the-element-index) instead.

docs/Inputs.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ const OrderEdit = () => (
647647
);
648648
```
649649

650-
**Tip**: When used inside an `ArrayInput`, `<FormDataConsumer>` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:
650+
**Tip**: When used inside an `<ArrayInput>`, `<FormDataConsumer>` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:
651651

652652
```tsx
653653
import { FormDataConsumer } from 'react-admin';
@@ -682,6 +682,8 @@ const PostEdit = () => (
682682

683683
**Tip:** TypeScript users will notice that `scopedFormData` is typed as an optional parameter. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, this parameter will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that this parameter will be defined.
684684

685+
**Tip:** If you need to access the *effective* source of an input inside an `<ArrayInput>`, for example to change the value programmatically using `setValue`, you will need to leverage the [`useSourceContext` hook](./ArrayInput#changing-an-items-value-programmatically).
686+
685687
## Hiding Inputs Based On Other Inputs
686688

687689
You may want to display or hide inputs based on the value of another input - for instance, show an `email` input only if the `hasEmail` boolean input has been ticked to `true`.

docs/ReferenceManyInput.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,63 @@ const ProductEdit = () => (
300300
- `<ReferenceManyInput>` cannot be used with `undoable` mutations in a `<Create>` view.
301301
- `<ReferenceManyInput>` cannot have a `<ReferenceOneInput>` or a `<ReferenceManyToManyInput>` as one of its children.
302302
- `<ReferenceManyInput>` does not support server side validation.
303+
304+
## Changing An Item's Value Programmatically
305+
306+
You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.
307+
308+
However you need to know the `name` under which the input was registered in the form, and this name is dynamically generated depending on the index of the item in the array.
309+
310+
To get the name of the input for a given index, you can leverage the `SourceContext` created by react-admin, which can be accessed using the `useSourceContext` hook.
311+
312+
This context provides a `getSource` function that returns the effective `source` for an input in the current context, which you can use as input name for `setValue`.
313+
314+
Here is an example where we leverage `getSource` and `setValue` to prefill the email input when the 'Prefill email' button is clicked:
315+
316+
{% raw %}
317+
318+
```tsx
319+
import { SimpleFormIterator, TextInput, useSourceContext } from 'react-admin';
320+
import { ReferenceManyInput } from '@react-admin/ra-relationships';
321+
import { useFormContext } from 'react-hook-form';
322+
import { Button } from '@mui/material';
323+
324+
const PrefillEmail = () => {
325+
const sourceContext = useSourceContext();
326+
const { setValue, getValues } = useFormContext();
327+
328+
const onClick = () => {
329+
const firstName = getValues(sourceContext.getSource('first_name'));
330+
const lastName = getValues(sourceContext.getSource('last_name'));
331+
const email = `${
332+
firstName ? firstName.toLowerCase() : ''
333+
}.${lastName ? lastName.toLowerCase() : ''}@school.com`;
334+
setValue(sourceContext.getSource('email'), email);
335+
};
336+
337+
return (
338+
<Button onClick={onClick} size="small" sx={{ minWidth: 140 }}>
339+
Prefill email
340+
</Button>
341+
);
342+
};
343+
344+
const StudentsInput = () => (
345+
<ReferenceManyInput
346+
reference="students"
347+
target="teacher_id"
348+
sort={{ field: 'last_name', order: 'ASC' }}
349+
>
350+
<SimpleFormIterator inline disableReordering>
351+
<TextInput source="first_name" helperText={false} />
352+
<TextInput source="last_name" helperText={false} />
353+
<TextInput source="email" helperText={false} />
354+
<PrefillEmail />
355+
</SimpleFormIterator>
356+
</ReferenceManyInput>
357+
);
358+
```
359+
360+
{% endraw %}
361+
362+
**Tip:** If you only need the item's index, you can leverage the [`useSimpleFormIteratorItem` hook](./SimpleFormIterator.md#getting-the-element-index) instead.

docs/ReferenceOneInput.md

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -234,58 +234,70 @@ Name of the field carrying the relationship on the referenced resource. For inst
234234
</ReferenceOneInput>
235235
```
236236

237-
## Customizing The Child Inputs
237+
## Limitations
238+
239+
- `<ReferenceOneInput>` cannot be used inside an `<ArrayInput>` or a `<ReferenceManyInput>`.
240+
- `<ReferenceOneInput>` cannot have a `<ReferenceManyInput>` or a `<ReferenceManyToManyInput>` as one of its children.
241+
- `<ReferenceOneInput>` does not support server side validation.
242+
243+
## Changing An Item's Value Programmatically
244+
245+
You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change the reference record's value programmatically.
238246

239-
`<ReferenceOneInput>` works by cloning its children and overriding their `source` prop, to add a temporary field name prefix. This means that, if you need to nest your inputs inside another component, you will need to propagate the `source` prop to them.
247+
However you need to know the `name` under which the inputs were registered in the form, and these names are dynamically generated by `<ReferenceOneInput>`.
240248

241-
In this example, the `<TextInput>` component is wrapped inside a `<MyCustomInput>` component. That adds an icon and additional styling.
249+
To get the name of a specific input, you can leverage the `SourceContext` created by react-admin, which can be accessed using the `useSourceContext` hook.
250+
251+
This context provides a `getSource` function that returns the effective `source` for an input in the current context, which you can use as input name for `setValue`.
252+
253+
Here is an example where we leverage `getSource` and `setValue` to update some of the book details when the 'Update book details' button is clicked:
242254

243255
{% raw %}
256+
244257
```tsx
245-
import AccountCircle from '@mui/icons-material/AccountCircle';
246-
import AutoStoriesIcon from '@mui/icons-material/AutoStories';
247-
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
248-
import ClassIcon from '@mui/icons-material/Class';
249-
import LanguageIcon from '@mui/icons-material/Language';
250-
import { Box, SxProps } from '@mui/material';
251-
import * as React from 'react';
252-
import { TextInput } from 'react-admin';
258+
import { NumberInput, TextInput, useSourceContext } from 'react-admin';
253259
import { ReferenceOneInput } from '@react-admin/ra-relationships';
254-
255-
const MyCustomInput = ({
256-
source,
257-
icon: Icon,
258-
}: {
259-
source: string;
260-
icon: React.FunctionComponent<{ sx?: SxProps }>;
261-
}) => (
262-
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
263-
<Icon sx={{ color: 'action.active', mr: 1.5, my: 1 }} />
264-
<TextInput
265-
source={source} // Propagate the source prop to the real input
266-
variant="standard"
267-
size="small"
268-
helperText={false}
269-
/>
270-
</Box>
271-
);
272-
273-
export const CustomInputs = () => (
274-
<ReferenceOneInput reference="book_details" target="book_id">
275-
<MyCustomInput source="year" icon={CalendarMonthIcon} />
276-
<MyCustomInput source="author" icon={AccountCircle} />
277-
<MyCustomInput source="country" icon={LanguageIcon} />
278-
<MyCustomInput source="genre" icon={ClassIcon} />
279-
<MyCustomInput source="pages" icon={AutoStoriesIcon} />
260+
import { useFormContext } from 'react-hook-form';
261+
import { Button, Stack, Box } from '@mui/material';
262+
263+
const UpdateBookDetails = () => {
264+
const sourceContext = useSourceContext();
265+
const { setValue } = useFormContext();
266+
267+
const onClick = () => {
268+
// Generate random values for year and pages
269+
const year = 1000 + Math.floor(Math.random() * 1000);
270+
const pages = 100 + Math.floor(Math.random() * 900);
271+
setValue(sourceContext.getSource('year'), year);
272+
setValue(sourceContext.getSource('pages'), pages);
273+
};
274+
275+
return (
276+
<Button onClick={onClick} size="small" sx={{ maxWidth: 200 }}>
277+
Update book details
278+
</Button>
279+
);
280+
};
281+
282+
const BookDetails = () => (
283+
<ReferenceOneInput
284+
reference="book_details"
285+
target="book_id"
286+
sort={sort}
287+
filter={filter}
288+
>
289+
<Stack direction="row" spacing={2}>
290+
<Box>
291+
<NumberInput source="year" />
292+
<TextInput source="author" />
293+
<TextInput source="country" />
294+
<TextInput source="genre" />
295+
<NumberInput source="pages" />
296+
</Box>
297+
<UpdateBookDetails />
298+
</Stack>
280299
</ReferenceOneInput>
281300
);
282301
```
283-
{% endraw %}
284-
285-
![ReferenceOneInput with custom inputs](https://react-admin-ee.marmelab.com/assets/ra-relationships/latest/reference-one-input-custom-inputs.png)
286302

287-
## Limitations
288-
289-
- `<ReferenceOneInput>` cannot be used inside an `<ArrayInput>` or a `<ReferenceManyInput>`.
290-
- `<ReferenceOneInput>` cannot have a `<ReferenceManyInput>` or a `<ReferenceManyToManyInput>` as one of its children.
291-
- `<ReferenceOneInput>` does not support server side validation.
303+
{% endraw %}

docs/SimpleFormIterator.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,3 +430,44 @@ This property accepts the following subclasses:
430430
| `RaSimpleFormIterator-inline` | Applied to rows when `inline` is true |
431431
| `RaSimpleFormIterator-line` | Applied to each row |
432432
| `RaSimpleFormIterator-list` | Applied to the `<ul>` element |
433+
434+
## Getting The Element Index
435+
436+
Inside a `<SimpleFormIterator>`, you can access the index of the current element using the `useSimpleFormIteratorItem` hook.
437+
438+
{% raw %}
439+
440+
```tsx
441+
import {
442+
TextInput,
443+
ArrayInput,
444+
SimpleFormIterator,
445+
useSimpleFormIteratorItem,
446+
} from 'react-admin';
447+
import { Typography } from '@mui/material';
448+
449+
const IndexField = () => {
450+
const { index } = useSimpleFormIteratorItem();
451+
return (
452+
<Typography variant="body2" sx={{ alignSelf: 'center' }}>
453+
#{index + 1}:
454+
</Typography>
455+
);
456+
};
457+
458+
const UserArray = () => (
459+
<ArrayInput source="items">
460+
<SimpleFormIterator inline>
461+
<IndexField />
462+
<TextInput source="name" helperText={false} />
463+
<TextInput source="role" helperText={false} />
464+
</SimpleFormIterator>
465+
</ArrayInput>
466+
);
467+
```
468+
469+
{% endraw %}
470+
471+
**Tip:** This hook also returns the total number of elements (`total`).
472+
473+
**Tip:** If you need the index to change the value of an input programmatically, you should use the [`useSourceContext` hook](./ArrayInput.md#changing-an-items-value-programmatically) instead.

docs/TranslatableInputs.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,53 @@ You can add validators to any of the inputs inside a `TranslatableInputs`. If an
198198
<RichTextInput source="description" validate={[maxLength(100)]} />
199199
</TranslatableInputs>
200200
```
201+
202+
## Changing The Value Programmatically
203+
204+
You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an input's value programmatically.
205+
206+
However you need to know the `name` under which the input was registered in the form, and this name is dynamically generated depending on the locale.
207+
208+
To get the name of the input for a given locale, you can leverage the `SourceContext` created by react-admin, which can be accessed using the `useSourceContext` hook.
209+
210+
This context provides a `getSource` function that returns the effective `source` for an input in the current context, which you can use as input name for `setValue`.
211+
212+
Here is an example where we leverage `getSource` and `setValue` to pre-fill the 'description' input using the value of the 'title' input when the corresponding button is clicked:
213+
214+
{% raw %}
215+
216+
```tsx
217+
import { TranslatableInputs, TextInput, useSourceContext } from 'react-admin';
218+
import { useFormContext } from 'react-hook-form';
219+
import { Button } from '@mui/material';
220+
221+
const PrefillWithTitleButton = () => {
222+
const sourceContext = useSourceContext();
223+
const { setValue, getValues } = useFormContext();
224+
225+
const onClick = () => {
226+
setValue(
227+
// sourceContext.getSource('description') will for instance return
228+
// 'description.en'
229+
sourceContext.getSource('description'),
230+
getValues(sourceContext.getSource('title'))
231+
);
232+
};
233+
234+
return (
235+
<Button onClick={onClick} size="small" sx={{ maxWidth: 140 }}>
236+
Prefill with title
237+
</Button>
238+
);
239+
};
240+
241+
const MyInputs = () => (
242+
<TranslatableInputs locales={['en', 'fr']}>
243+
<TextInput source="title" />
244+
<TextInput source="description" helperText={false} />
245+
<PrefillWithTitleButton />
246+
</TranslatableInputs>
247+
);
248+
```
249+
250+
{% endraw %}

0 commit comments

Comments
 (0)