Skip to content

Commit caa31e4

Browse files
authored
Merge pull request #10955 from marmelab/array-input-base
Introduce `<ArrayInputBase>`, `<SimpleFomIteratorBase>` and `<SimpleFormIteratorItemBase>`
2 parents f9c1cf0 + 84808da commit caa31e4

35 files changed

+2271
-436
lines changed

docs_headless/astro.config.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ export default defineConfig({
173173
},
174174
{
175175
label: 'Inputs',
176-
items: ['inputs', 'useinput'],
176+
items: [
177+
'inputs',
178+
'useinput',
179+
'arrayinputbase',
180+
'simpleformiteratorbase',
181+
],
177182
},
178183
{
179184
label: 'Preferences',
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
layout: default
3+
title: "<ArrayInputBase>"
4+
---
5+
6+
`<ArrayInputBase>` allows editing of embedded arrays, like the `items` field in the following `order` record:
7+
8+
```json
9+
{
10+
"id": 1,
11+
"date": "2022-08-30",
12+
"customer": "John Doe",
13+
"items": [
14+
{
15+
"name": "Office Jeans",
16+
"price": 45.99,
17+
"quantity": 1,
18+
},
19+
{
20+
"name": "Black Elegance Jeans",
21+
"price": 69.99,
22+
"quantity": 2,
23+
},
24+
{
25+
"name": "Slim Fit Jeans",
26+
"price": 55.99,
27+
"quantity": 1,
28+
},
29+
],
30+
}
31+
```
32+
33+
## Usage
34+
35+
`<ArrayInputBase>` expects a single child, which must be a *form iterator* component. A form iterator is a component rendering a field array (the object returned by react-hook-form's [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray)). You can build such component using [the `<SimpleFormIteratorBase>`](./SimpleFormIteratorBase.md).
36+
37+
```tsx
38+
import { ArrayInputBase, EditBase, Form } from 'ra-core';
39+
import { MyFormIterator } from './MyFormIterator';
40+
import { DateInput } from './DateInput';
41+
import { NumberInput } from './NumberInput';
42+
import { TextInput } from './TextInput';
43+
44+
export const OrderEdit = () => (
45+
<EditBase>
46+
<Form>
47+
<DateInput source="date" />
48+
<div>
49+
<div>Items:</div>
50+
<ArrayInputBase source="items">
51+
<MyFormIterator>
52+
<TextInput source="name" />
53+
<NumberInput source="price" />
54+
<NumberInput source="quantity" />
55+
</MyFormIterator>
56+
</ArrayInputBase>
57+
</div>
58+
<button type="submit">Save</button>
59+
</Form>
60+
</EditBase>
61+
)
62+
```
63+
64+
**Note**: Setting [`shouldUnregister`](https://react-hook-form.com/docs/useform#shouldUnregister) on a form should be avoided when using `<ArrayInputBase>` (which internally uses `useFieldArray`) as the unregister function gets called after input unmount/remount and reorder. This limitation is mentioned in the react-hook-form [documentation](https://react-hook-form.com/docs/usecontroller#props). If you are in such a situation, you can use the [`transform`](./EditBase.md#transform) prop to manually clean the submitted values.
65+
66+
## Props
67+
68+
| Prop | Required | Type | Default | Description |
69+
|-----------------| -------- |---------------------------| ------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
70+
| `source` | Required | `string` | - | Name of the entity property to use for the input value |
71+
| `defaultValue` | Optional | `any` | - | Default value of the input. |
72+
| `validate` | Optional | `Function` &#124; `array` | - | Validation rules for the current property. See the [Validation Documentation](./Validation.md#per-input-validation-built-in-field-validators) for details. |
73+
74+
## Global validation
75+
76+
If you are using an `<ArrayInputBase>` inside a form with global validation, you need to shape the errors object returned by the `validate` function like an array too.
77+
78+
For instance, to display the following errors:
79+
80+
![ArrayInput global validation](../../img/ArrayInput-global-validation.png)
81+
82+
You need to return an errors object shaped like this:
83+
84+
```js
85+
{
86+
authors: [
87+
{},
88+
{
89+
name: 'A name is required',
90+
role: 'ra.validation.required' // translation keys are supported too
91+
},
92+
],
93+
}
94+
```
95+
96+
**Tip:** You can find a sample `validate` function that handles arrays in the [Form Validation documentation](./Validation.md#global-validation).
97+
98+
## Disabling The Input
99+
100+
`<ArrayInputBase>` does not support the `disabled` and `readOnly` props.
101+
102+
If you need to disable the input, make sure the children are either `disabled` and `readOnly`:
103+
104+
```jsx
105+
import { ArrayInputBase, EditBase, Form } from 'ra-core';
106+
import { MyFormIterator } from './MyFormIterator';
107+
import { DateInput } from './DateInput';
108+
import { NumberInput } from './NumberInput';
109+
import { TextInput } from './TextInput';
110+
111+
const OrderEdit = () => (
112+
<EditBase>
113+
<Form>
114+
<TextInput source="customer" />
115+
<DateInput source="date" />
116+
<div>
117+
<div>Items:</div>
118+
<ArrayInputBase source="items">
119+
<MyFormIterator inline disabled>
120+
<TextInput source="name" readOnly/>
121+
<NumberInput source="price" readOnly />
122+
<NumberInput source="quantity" readOnly />
123+
</MyFormIterator>
124+
</ArrayInputBase>
125+
</div>
126+
<button type="submit">Save</button>
127+
</Form>
128+
</EditBase>
129+
);
130+
```
131+
132+
## Changing An Item's Value Programmatically
133+
134+
You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.
135+
136+
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.
137+
138+
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.
139+
140+
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`.
141+
142+
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:
143+
144+
```tsx
145+
import { ArrayInputBase, useSourceContext } from 'ra-core';
146+
import { useFormContext } from 'react-hook-form';
147+
import { MyFormIterator } from './MyFormIterator';
148+
149+
const MakeAdminButton = () => {
150+
const sourceContext = useSourceContext();
151+
const { setValue } = useFormContext();
152+
153+
const onClick = () => {
154+
// sourceContext.getSource('role') will for instance return
155+
// 'users.0.role'
156+
setValue(sourceContext.getSource('role'), 'admin');
157+
};
158+
159+
return (
160+
<button onClick={onClick}>
161+
Make admin
162+
</button>
163+
);
164+
};
165+
166+
const UserArray = () => (
167+
<ArrayInputBase source="users">
168+
<MyFormIterator inline>
169+
<TextInput source="name" helperText={false} />
170+
<TextInput source="role" helperText={false} />
171+
<MakeAdminButton />
172+
</MyFormIterator>
173+
</ArrayInputBase>
174+
);
175+
```
176+
177+
**Tip:** If you only need the item's index, you can leverage the `useSimpleFormIteratorItem` hook instead.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
layout: default
3+
title: "<SimpleFormIteratorBase>"
4+
---
5+
6+
`<SimpleFormIteratorBase>` helps building a component that lets users edit, add, remove and reorder sub-records. It is designed to be used as a child of [`<ArrayInputBase>`](./ArrayInputBase.md) or [`<ReferenceManyInputBase>`](https://react-admin-ee.marmelab.com/documentation/ra-core-ee#referencemanyinputbase). You can also use it within an `ArrayInputContext` containing a *field array*, i.e. the value returned by [react-hook-form's `useFieldArray` hook](https://react-hook-form.com/docs/usefieldarray).
7+
8+
## Usage
9+
10+
Here's how one could implement a minimal `SimpleFormIterator` using `<SimpleFormIteratorBase>`:
11+
12+
```tsx
13+
import {
14+
SimpleFormIteratorBase,
15+
SimpleFormIteratorItemBase,
16+
useArrayInput,
17+
useFieldValue,
18+
useSimpleFormIterator,
19+
useSimpleFormIteratorItem,
20+
useWrappedSource,
21+
type SimpleFormIteratorBaseProps
22+
} from 'ra-core';
23+
24+
export const SimpleFormIterator = ({ children, ...props }: SimpleFormIteratorBaseProps) => {
25+
const { fields } = useArrayInput(props);
26+
// Get the parent source by passing an empty string as source
27+
const source = useWrappedSource('');
28+
const records = useFieldValue({ source });
29+
30+
return (
31+
<SimpleFormIteratorBase {...props}>
32+
<ul>
33+
{fields.map((member, index) => (
34+
<SimpleFormIteratorItemBase
35+
key={member.id}
36+
index={index}
37+
record={record}
38+
>
39+
<li>
40+
{children}
41+
<RemoveItemButton />
42+
</li>
43+
</SimpleFormIteratorItemBase>
44+
))}
45+
</ul>
46+
<AddItemButton />
47+
</SimpleFormIteratorBase>
48+
)
49+
}
50+
51+
const RemoveItemButton = () => {
52+
const { remove } = useSimpleFormIteratorItem();
53+
return (
54+
<button type="button" onClick={() => remove()}>Remove</button>
55+
)
56+
}
57+
58+
const AddItemButton = () => {
59+
const { add } = useSimpleFormIterator();
60+
return (
61+
<button type="button" onClick={() => add()}>Add</button>
62+
)
63+
}
64+
```
65+
66+
## Props
67+
68+
| Prop | Required | Type | Default | Description |
69+
|-------------------|----------|----------------|-----------------------|-----------------------------------------------|
70+
| `children` | Optional | `ReactElement` | - | List of inputs to display for each array item |
20.2 KB
Loading
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as React from 'react';
2+
import fakeRestDataProvider from 'ra-data-fakerest';
3+
import { TestMemoryRouter } from '../../routing';
4+
import { EditBase } from '../edit';
5+
import {
6+
Admin,
7+
DataTable,
8+
TextInput,
9+
SimpleFormIterator,
10+
SimpleForm,
11+
} from '../../test-ui';
12+
import { ListBase } from '../list';
13+
import { Resource } from '../../core';
14+
import { ArrayInputBase } from './ArrayInputBase';
15+
16+
export default { title: 'ra-core/controller/input/ArrayInputBase' };
17+
18+
export const Basic = () => (
19+
<TestMemoryRouter initialEntries={['/posts/1']}>
20+
<Admin
21+
dataProvider={fakeRestDataProvider({
22+
posts: [
23+
{
24+
id: 1,
25+
title: 'Post 1',
26+
tags: [
27+
{ name: 'Tag 1', color: 'red' },
28+
{ name: 'Tag 2', color: 'blue' },
29+
],
30+
},
31+
{ id: 2, title: 'Post 2' },
32+
],
33+
})}
34+
>
35+
<Resource
36+
name="posts"
37+
list={
38+
<ListBase>
39+
<DataTable>
40+
<DataTable.Col source="title" />
41+
<DataTable.Col
42+
label="Tags"
43+
render={record =>
44+
record.tags
45+
? record.tags
46+
.map(tag => tag.name)
47+
.join(', ')
48+
: ''
49+
}
50+
/>
51+
</DataTable>
52+
</ListBase>
53+
}
54+
edit={
55+
<EditBase>
56+
<SimpleForm>
57+
<TextInput source="title" />
58+
<div>
59+
<div>Tags:</div>
60+
<ArrayInputBase source="tags">
61+
<SimpleFormIterator>
62+
<TextInput source="name" />
63+
<TextInput source="color" />
64+
</SimpleFormIterator>
65+
</ArrayInputBase>
66+
</div>
67+
</SimpleForm>
68+
</EditBase>
69+
}
70+
/>
71+
</Admin>
72+
</TestMemoryRouter>
73+
);

0 commit comments

Comments
 (0)