Skip to content

Commit

Permalink
Merge pull request #10547 from marmelab/doc-useSimpleFormIteratorItem
Browse files Browse the repository at this point in the history
[Doc] Document `useSourceContext` and improve `useSimpleFormIteratorItem` documentation
  • Loading branch information
djhi authored Feb 26, 2025
2 parents 518fbb6 + 6261fd9 commit fd26e65
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 48 deletions.
51 changes: 51 additions & 0 deletions docs/ArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,54 @@ const OrderEdit = () => (
</Edit>
);
```

## Changing An Item's Value Programmatically

You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.

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.

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.

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`.

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:

{% raw %}

```tsx
import { ArrayInput, SimpleFormIterator, TextInput, useSourceContext } from 'react-admin';
import { useFormContext } from 'react-hook-form';
import { Button } from '@mui/material';

const MakeAdminButton = () => {
const sourceContext = useSourceContext();
const { setValue } = useFormContext();

const onClick = () => {
// sourceContext.getSource('role') will for instance return
// 'users.0.role'
setValue(sourceContext.getSource('role'), 'admin');
};

return (
<Button onClick={onClick} size="small" sx={{ minWidth: 120 }}>
Make admin
</Button>
);
};

const UserArray = () => (
<ArrayInput source="users">
<SimpleFormIterator inline>
<TextInput source="name" helperText={false} />
<TextInput source="role" helperText={false} />
<MakeAdminButton />
</SimpleFormIterator>
</ArrayInput>
);
```

{% endraw %}

**Tip:** If you only need the item's index, you can leverage the [`useSimpleFormIteratorItem` hook](./SimpleFormIterator.md#getting-the-element-index) instead.
4 changes: 3 additions & 1 deletion docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ const OrderEdit = () => (
);
```

**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:
**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:

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

**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.

**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).

## Hiding Inputs Based On Other Inputs

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`.
Expand Down
60 changes: 60 additions & 0 deletions docs/ReferenceManyInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,63 @@ const ProductEdit = () => (
- `<ReferenceManyInput>` cannot be used with `undoable` mutations in a `<Create>` view.
- `<ReferenceManyInput>` cannot have a `<ReferenceOneInput>` or a `<ReferenceManyToManyInput>` as one of its children.
- `<ReferenceManyInput>` does not support server side validation.

## Changing An Item's Value Programmatically

You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.

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.

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.

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`.

Here is an example where we leverage `getSource` and `setValue` to prefill the email input when the 'Prefill email' button is clicked:

{% raw %}

```tsx
import { SimpleFormIterator, TextInput, useSourceContext } from 'react-admin';
import { ReferenceManyInput } from '@react-admin/ra-relationships';
import { useFormContext } from 'react-hook-form';
import { Button } from '@mui/material';

const PrefillEmail = () => {
const sourceContext = useSourceContext();
const { setValue, getValues } = useFormContext();

const onClick = () => {
const firstName = getValues(sourceContext.getSource('first_name'));
const lastName = getValues(sourceContext.getSource('last_name'));
const email = `${
firstName ? firstName.toLowerCase() : ''
}.${lastName ? lastName.toLowerCase() : ''}@school.com`;
setValue(sourceContext.getSource('email'), email);
};

return (
<Button onClick={onClick} size="small" sx={{ minWidth: 140 }}>
Prefill email
</Button>
);
};

const StudentsInput = () => (
<ReferenceManyInput
reference="students"
target="teacher_id"
sort={{ field: 'last_name', order: 'ASC' }}
>
<SimpleFormIterator inline disableReordering>
<TextInput source="first_name" helperText={false} />
<TextInput source="last_name" helperText={false} />
<TextInput source="email" helperText={false} />
<PrefillEmail />
</SimpleFormIterator>
</ReferenceManyInput>
);
```

{% endraw %}

**Tip:** If you only need the item's index, you can leverage the [`useSimpleFormIteratorItem` hook](./SimpleFormIterator.md#getting-the-element-index) instead.
102 changes: 57 additions & 45 deletions docs/ReferenceOneInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,58 +234,70 @@ Name of the field carrying the relationship on the referenced resource. For inst
</ReferenceOneInput>
```

## Customizing The Child Inputs
## Limitations

- `<ReferenceOneInput>` cannot be used inside an `<ArrayInput>` or a `<ReferenceManyInput>`.
- `<ReferenceOneInput>` cannot have a `<ReferenceManyInput>` or a `<ReferenceManyToManyInput>` as one of its children.
- `<ReferenceOneInput>` does not support server side validation.

## Changing An Item's Value Programmatically

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.

`<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.
However you need to know the `name` under which the inputs were registered in the form, and these names are dynamically generated by `<ReferenceOneInput>`.

In this example, the `<TextInput>` component is wrapped inside a `<MyCustomInput>` component. That adds an icon and additional styling.
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.

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`.

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:

{% raw %}

```tsx
import AccountCircle from '@mui/icons-material/AccountCircle';
import AutoStoriesIcon from '@mui/icons-material/AutoStories';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import ClassIcon from '@mui/icons-material/Class';
import LanguageIcon from '@mui/icons-material/Language';
import { Box, SxProps } from '@mui/material';
import * as React from 'react';
import { TextInput } from 'react-admin';
import { NumberInput, TextInput, useSourceContext } from 'react-admin';
import { ReferenceOneInput } from '@react-admin/ra-relationships';

const MyCustomInput = ({
source,
icon: Icon,
}: {
source: string;
icon: React.FunctionComponent<{ sx?: SxProps }>;
}) => (
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<Icon sx={{ color: 'action.active', mr: 1.5, my: 1 }} />
<TextInput
source={source} // Propagate the source prop to the real input
variant="standard"
size="small"
helperText={false}
/>
</Box>
);

export const CustomInputs = () => (
<ReferenceOneInput reference="book_details" target="book_id">
<MyCustomInput source="year" icon={CalendarMonthIcon} />
<MyCustomInput source="author" icon={AccountCircle} />
<MyCustomInput source="country" icon={LanguageIcon} />
<MyCustomInput source="genre" icon={ClassIcon} />
<MyCustomInput source="pages" icon={AutoStoriesIcon} />
import { useFormContext } from 'react-hook-form';
import { Button, Stack, Box } from '@mui/material';

const UpdateBookDetails = () => {
const sourceContext = useSourceContext();
const { setValue } = useFormContext();

const onClick = () => {
// Generate random values for year and pages
const year = 1000 + Math.floor(Math.random() * 1000);
const pages = 100 + Math.floor(Math.random() * 900);
setValue(sourceContext.getSource('year'), year);
setValue(sourceContext.getSource('pages'), pages);
};

return (
<Button onClick={onClick} size="small" sx={{ maxWidth: 200 }}>
Update book details
</Button>
);
};

const BookDetails = () => (
<ReferenceOneInput
reference="book_details"
target="book_id"
sort={sort}
filter={filter}
>
<Stack direction="row" spacing={2}>
<Box>
<NumberInput source="year" />
<TextInput source="author" />
<TextInput source="country" />
<TextInput source="genre" />
<NumberInput source="pages" />
</Box>
<UpdateBookDetails />
</Stack>
</ReferenceOneInput>
);
```
{% endraw %}

![ReferenceOneInput with custom inputs](https://react-admin-ee.marmelab.com/assets/ra-relationships/latest/reference-one-input-custom-inputs.png)

## Limitations

- `<ReferenceOneInput>` cannot be used inside an `<ArrayInput>` or a `<ReferenceManyInput>`.
- `<ReferenceOneInput>` cannot have a `<ReferenceManyInput>` or a `<ReferenceManyToManyInput>` as one of its children.
- `<ReferenceOneInput>` does not support server side validation.
{% endraw %}
41 changes: 41 additions & 0 deletions docs/SimpleFormIterator.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,44 @@ This property accepts the following subclasses:
| `RaSimpleFormIterator-inline` | Applied to rows when `inline` is true |
| `RaSimpleFormIterator-line` | Applied to each row |
| `RaSimpleFormIterator-list` | Applied to the `<ul>` element |
## Getting The Element Index
Inside a `<SimpleFormIterator>`, you can access the index of the current element using the `useSimpleFormIteratorItem` hook.
{% raw %}
```tsx
import {
TextInput,
ArrayInput,
SimpleFormIterator,
useSimpleFormIteratorItem,
} from 'react-admin';
import { Typography } from '@mui/material';

const IndexField = () => {
const { index } = useSimpleFormIteratorItem();
return (
<Typography variant="body2" sx={{ alignSelf: 'center' }}>
#{index + 1}:
</Typography>
);
};

const UserArray = () => (
<ArrayInput source="items">
<SimpleFormIterator inline>
<IndexField />
<TextInput source="name" helperText={false} />
<TextInput source="role" helperText={false} />
</SimpleFormIterator>
</ArrayInput>
);
```
{% endraw %}
**Tip:** This hook also returns the total number of elements (`total`).
**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.
50 changes: 50 additions & 0 deletions docs/TranslatableInputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,53 @@ You can add validators to any of the inputs inside a `TranslatableInputs`. If an
<RichTextInput source="description" validate={[maxLength(100)]} />
</TranslatableInputs>
```

## Changing The Value Programmatically

You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an input's value programmatically.

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.

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.

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`.

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:

{% raw %}

```tsx
import { TranslatableInputs, TextInput, useSourceContext } from 'react-admin';
import { useFormContext } from 'react-hook-form';
import { Button } from '@mui/material';

const PrefillWithTitleButton = () => {
const sourceContext = useSourceContext();
const { setValue, getValues } = useFormContext();

const onClick = () => {
setValue(
// sourceContext.getSource('description') will for instance return
// 'description.en'
sourceContext.getSource('description'),
getValues(sourceContext.getSource('title'))
);
};

return (
<Button onClick={onClick} size="small" sx={{ maxWidth: 140 }}>
Prefill with title
</Button>
);
};

const MyInputs = () => (
<TranslatableInputs locales={['en', 'fr']}>
<TextInput source="title" />
<TextInput source="description" helperText={false} />
<PrefillWithTitleButton />
</TranslatableInputs>
);
```

{% endraw %}
Loading

0 comments on commit fd26e65

Please sign in to comment.