Skip to content

Commit 622449b

Browse files
authored
Merge pull request #10979 from marmelab/doc/ra-relationships-core
doc(ra-relationships): Add documentation about ra-relationship core components
2 parents b4b7e2d + 4b95488 commit 622449b

File tree

6 files changed

+1392
-1
lines changed

6 files changed

+1392
-1
lines changed

docs_headless/astro.config.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,20 @@ export default defineConfig({
167167
'referencefieldbase',
168168
'referencemanycountbase',
169169
'referencemanyfieldbase',
170+
'referencemanytomanyfieldbase',
170171
'referenceonefieldbase',
171172
'usefieldvalue',
172173
],
173174
},
174175
{
175176
label: 'Inputs',
176-
items: ['inputs', 'useinput'],
177+
items: [
178+
'inputs',
179+
'referencemanyinputbase',
180+
'referencemanytomanyinputbase',
181+
'referenceoneinputbase',
182+
'useinput',
183+
],
177184
},
178185
{
179186
label: 'Preferences',
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
---
2+
title: "<ReferenceManyInputBase>"
3+
---
4+
Use `<ReferenceManyInputBase>` in an edition or creation views to edit one-to-many relationships, e.g. to edit the variants of a product in the product edition view.
5+
6+
`<ReferenceManyInputBase>` fetches the related records, and renders them in a sub-form. When users add, remove of update related records, the `<ReferenceManyInputBase>` component stores these changes locally. When the users actually submit the form, `<ReferenceManyInputBase>` computes a diff with the existing relationship, and sends the related changes (additions, deletions, and updates) to the server.
7+
8+
## Usage
9+
10+
An example one-to-many relationship can be found in ecommerce systems: a product has many variants.
11+
12+
```txt
13+
┌───────────────┐ ┌──────────────┐
14+
│ products │ │ variants │
15+
│---------------│ │--------------│
16+
│ id │───┐ │ id │
17+
│ name │ └──╼│ product_id │
18+
│ price │ │ sku │
19+
│ category_id │ │ size │
20+
└───────────────┘ │ color │
21+
│ stock │
22+
└──────────────┘
23+
```
24+
25+
You probably want to let users edit variants directly from the product Edition view (instead of having to go to the variant Edition view). `<ReferenceManyInputBase>` allows to do that.
26+
27+
```jsx
28+
import { EditBase, Form, ReferenceInputBase } from 'ra-core';
29+
import {
30+
AutocompleteInput,
31+
TextInput,
32+
NumberInput,
33+
SelectInput,
34+
SimpleFormIterator
35+
} from 'my-react-admin-ui-library';
36+
import { ReferenceManyInputBase } from '@react-admin/ra-core-ee';
37+
38+
const ProductEdit = () => (
39+
<EditBase mutationMode="optimistic">
40+
<Form>
41+
<TextInput source="name" />
42+
<NumberInput source="price" />
43+
<ReferenceInputBase source="category_id" reference="categories">
44+
<AutocompleteInput />
45+
</ReferenceInputBase>
46+
<ReferenceManyInputBase reference="variants" target="product_id">
47+
<SimpleFormIterator>
48+
<TextInput source="sku" />
49+
<SelectInput source="size" choices={sizes} />
50+
<SelectInput source="color" choices={colors} />
51+
<NumberInput source="stock" defaultValue={0} />
52+
</SimpleFormIterator>
53+
</ReferenceManyInputBase>
54+
</Form>
55+
</EditBase>
56+
);
57+
```
58+
59+
`<ReferenceManyInputBase>` requires a `reference` and a `target` prop to know which entity to fetch, and a child component (usually a `<SimpleFormIterator>`) to edit the relationship.
60+
61+
`<ReferenceManyInputBase>` persists the changes in the reference records (variants in the above example) after persisting the changes in the main resource (product in the above example). This means that you can also use `<ReferenceManyInputBase>` in `<CreateBase>` views.
62+
63+
**Tip**: `<ReferenceManyInputBase>` cannot be used with `undoable` mutations. You have to set `mutationMode="optimistic"` or `mutationMode="pessimistic"` in the parent `<EditBase>` or `<CreateBase>`, as in the example above.
64+
65+
## Props
66+
67+
| Prop | Required | Type | Default | Description |
68+
| ----------------- | -------- | ------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69+
| `target` | Required | `string` | - | Target field carrying the relationship on the referenced resource, e.g. 'user_id' |
70+
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'books' |
71+
| `children` | Optional | `Element` | - | One or several elements that render a list of records based on a `ListContext` |
72+
| `defaultValue` | Optional | `array` | - | Default value of the input. |
73+
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
74+
| `mutationOptions` | Optional | `UseMutationOptions` | - | Options for the mutations (`create`, `update` and `delete`) |
75+
| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
76+
| `queryOptions` | Optional | `UseQueryOptions` | - | Options for the queries (`getManyReferences`) |
77+
| `rankSource` | Optional | `string` | - | Name of the field used to store the rank of each item. When defined, it enables reordering of the items. |
78+
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when fetching the related records, passed to `getManyReference()` |
79+
| `source` | Optional | `string` | `id` | Name of the field that carries the identity of the current record, used as origin for the relationship |
80+
| `validate` | Optional | `Function` &#124; `array` | - | Validation rules for the array. See the [Validation Documentation](https://marmelab.com/ra-core/validation) for details. |
81+
82+
## `children`
83+
84+
`<ReferenceManyInputBase>` creates an `ArrayInputContext`, so it accepts the same type of children as `<ArrayInput>`: a Form iterator. React-admin bundles one such iterator: `<SimpleFormIterator>`. It renders one row for each related record, giving the user the ability to add, remove, or edit related records.
85+
86+
```jsx
87+
<ReferenceManyInputBase reference="variants" target="product_id">
88+
<SimpleFormIterator>
89+
<TextInput source="sku" />
90+
<SelectInput source="size" choices={sizes} />
91+
<SelectInput source="color" choices={colors} />
92+
<NumberInput source="stock" defaultValue={0} />
93+
</SimpleFormIterator>
94+
</ReferenceManyInputBase>
95+
```
96+
97+
Check out [the `<SimpleFormIterator>` documentation](https://marmelab.com/react-admin/SimpleFormIterator.html) for more details.
98+
99+
## `defaultValue`
100+
101+
When the current record has no related records, `<ReferenceManyInputBase>` renders an empty list with an "Add" button to add related records.
102+
103+
You can use the `defaultValue` prop to populate the list of related records in that case. It must be an array of objects.
104+
105+
```jsx
106+
<ReferenceManyInputBase
107+
reference="variants"
108+
target="product_id"
109+
defaultValue={[
110+
{ sku: 'SKU_1', size: 'S', color: 'black', stock: 0 },
111+
{ sku: 'SKU_2', size: 'M', color: 'black', stock: 0 },
112+
{ sku: 'SKU_3', size: 'L', color: 'black', stock: 0 },
113+
{ sku: 'SKU_4', size: 'XL', color: 'black', stock: 0 },
114+
]}
115+
>
116+
<SimpleFormIterator>
117+
<TextInput source="sku" />
118+
<SelectInput source="size" choices={sizes} />
119+
<SelectInput source="color" choices={colors} />
120+
<NumberInput source="stock" defaultValue={0} />
121+
</SimpleFormIterator>
122+
</ReferenceManyInputBase>
123+
```
124+
125+
## `filter`
126+
127+
You can filter the query used to populate the current values. Use the `filter` prop for that.
128+
129+
```jsx
130+
<ReferenceManyInputBase
131+
reference="variants"
132+
target="product_id"
133+
filter={{ is_published: true }}
134+
>
135+
...
136+
</ReferenceManyInputBase>
137+
```
138+
139+
## `perPage`
140+
141+
By default, ra-core-ee restricts the possible values to 25 and displays no pagination control. You can change the limit by setting the `perPage` prop:
142+
143+
```jsx
144+
<ReferenceManyInputBase reference="variants" target="product_id" perPage={10}>
145+
...
146+
</ReferenceManyInputBase>
147+
```
148+
149+
## `rankSource`
150+
151+
If the Form iterator you use as `ReferenceManyInputBase` children (e.g. `<SimpleFormIterator>`) provides controls to reorder the items in the list and the related records have a numeric rank field, you can enable the reordering feature by setting the `rankSource` prop.
152+
153+
For example, if the variants have a `rank` field, you can set the `rankSource` prop like this:
154+
155+
```jsx
156+
<ReferenceManyInputBase
157+
reference="variants"
158+
target="product_id"
159+
rankSource="rank"
160+
>
161+
<SimpleFormIterator>
162+
<TextInput source="sku" />
163+
<SelectInput source="size" choices={sizes} />
164+
<SelectInput source="color" choices={colors} />
165+
<NumberInput source="stock" defaultValue={0} />
166+
</SimpleFormIterator>
167+
</ReferenceManyInputBase>
168+
```
169+
170+
Now the variants will be ordered by rank, and whenever the user changes the order of the items, `<ReferenceManyInputBase>` will update the `rank` field of each item accordingly.
171+
172+
## `reference`
173+
174+
The name of the resource to fetch for the related records.
175+
176+
For instance, if you want to display the `variants` of a given `product`, the `reference` name should be `variants`:
177+
178+
```jsx
179+
<ReferenceManyInputBase reference="books" target="author_id">
180+
...
181+
</ReferenceManyInputBase>
182+
```
183+
184+
## `sort`
185+
186+
By default, related records appear ordered by id desc. You can change this order by setting the `sort` prop (an object with `field` and `order` properties).
187+
188+
```jsx
189+
<ReferenceManyInputBase
190+
reference="variants"
191+
target="product_id"
192+
sort={{ field: 'sku', order: 'ASC' }}
193+
>
194+
...
195+
</ReferenceManyInputBase>
196+
```
197+
198+
## `source`
199+
200+
By default, `<ReferenceManyInputBase>` fetches the `references` for which the `target` field equals the current record `id`. You can customize the field that carries the identity of the current record by setting the `source` prop.
201+
202+
```jsx
203+
<ReferenceManyInputBase reference="variants" target="product_id" source="_id">
204+
...
205+
</ReferenceManyInputBase>
206+
```
207+
208+
## `target`
209+
210+
Name of the field carrying the relationship on the referenced resource. For instance, if a `product` has many `variants`, and each variant resource exposes an `product_id` field, the `target` would be `author_id`.
211+
212+
```jsx
213+
<ReferenceManyInputBase reference="variants" target="product_id">
214+
...
215+
</ReferenceManyInputBase>
216+
```
217+
218+
## `validate`
219+
220+
Just like regular inputs, you can use the `validate` prop to define custom validation rules for the list of references.
221+
222+
```jsx
223+
import { minLength } from 'ra-core';
224+
225+
const ProductEdit = () => (
226+
<EditBase mutationMode="optimistic">
227+
<Form>
228+
<TextInput source="name" />
229+
<ReferenceInput source="category_id" reference="categories" />
230+
<ReferenceManyInputBase
231+
reference="variants"
232+
target="product_id"
233+
validate={[minLength(2, 'Please add at least 2 variants')]}
234+
>
235+
...
236+
</ReferenceManyInputBase>
237+
</Form>
238+
</EditBase>
239+
);
240+
```
241+
242+
## Limitations
243+
244+
- `<ReferenceManyInputBase>` cannot be used inside an `<ArrayInputBase>` or a `<ReferenceOneInputBase>`.
245+
- `<ReferenceManyInputBase>` cannot be used with `undoable` mutations in a `<CreateBase>` view.
246+
- `<ReferenceManyInputBase>` cannot have a `<ReferenceOneInputBase>` or a `<ReferenceManyToManyInputBase>` as one of its children.
247+
- `<ReferenceManyInputBase>` does not support server side validation.
248+
249+
## Changing An Item's Value Programmatically
250+
251+
252+
You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.
253+
254+
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.
255+
256+
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.
257+
258+
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`.
259+
260+
Here is an example where we leverage `getSource` and `setValue` to prefill the email input when the 'Prefill email' button is clicked:
261+
262+
```tsx
263+
import { useSourceContext } from 'ra-core';
264+
import { SimpleFormIterator, TextInput } from 'my-react-admin-ui-library';
265+
import { ReferenceManyInputBase } from '@react-admin/ra-core-ee';
266+
import { useFormContext } from 'react-hook-form';
267+
268+
const PrefillEmail = () => {
269+
const sourceContext = useSourceContext();
270+
const { setValue, getValues } = useFormContext();
271+
272+
const onClick = () => {
273+
const firstName = getValues(sourceContext.getSource('first_name'));
274+
const lastName = getValues(sourceContext.getSource('last_name'));
275+
const email = `${
276+
firstName ? firstName.toLowerCase() : ''
277+
}.${lastName ? lastName.toLowerCase() : ''}@school.com`;
278+
setValue(sourceContext.getSource('email'), email);
279+
};
280+
281+
return (
282+
<button onClick={onClick}>
283+
Prefill email
284+
</button>
285+
);
286+
};
287+
288+
const StudentsInput = () => (
289+
<ReferenceManyInputBase
290+
reference="students"
291+
target="teacher_id"
292+
sort={{ field: 'last_name', order: 'ASC' }}
293+
>
294+
<SimpleFormIterator>
295+
<TextInput source="first_name" helperText={false} />
296+
<TextInput source="last_name" helperText={false} />
297+
<TextInput source="email" helperText={false} />
298+
<PrefillEmail />
299+
</SimpleFormIterator>
300+
</ReferenceManyInputBase>
301+
);
302+
```
303+
304+
**Tip:** If you only need the item's index, you can leverage the [`useSimpleFormIteratorItem` hook](https://marmelab.com/react-admin/SimpleFormIterator.html#getting-the-element-index) instead.

0 commit comments

Comments
 (0)