Skip to content

Commit ab9fe9c

Browse files
authored
Merge pull request #8802 from marmelab/ArrayField-filterable
Add ability to change the sort, filter and selection of ArrayField
2 parents 11fbc6e + c39c9ce commit ab9fe9c

File tree

9 files changed

+441
-100
lines changed

9 files changed

+441
-100
lines changed

docs/ArrayField.md

+176-15
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ title: "The ArrayField Component"
55

66
# `<ArrayField>`
77

8-
Display a collection using `<Field>` child components.
8+
`<ArrayField>` renders an embedded array of objects.
99

10-
Ideal for embedded arrays of objects, e.g. `tags` and `backlinks` in the following `post` object:
10+
![ArrayField](./img/array-field.webp)
11+
12+
`<ArrayField>` creates a [`ListContext`](./useListContext.md) with the field value, and renders its children components - usually iterator components like [`<Datagrid>`](./Datagrid.md) or [`<SingleFieldList>`](./SingleFieldList.md).
13+
14+
## Usage
15+
16+
`<ArrayField>` is ideal for collections of objects, e.g. `tags` and `backlinks` in the following `post` object:
1117

1218
```js
1319
{
1420
id: 123,
15-
tags: [
16-
{ name: 'foo' },
17-
{ name: 'bar' }
18-
],
21+
title: 'Lorem Ipsum Sit Amet',
22+
tags: [{ name: 'dolor' }, { name: 'sit' }, { name: 'amet' }],
1923
backlinks: [
2024
{
2125
uuid: '34fdf393-f449-4b04-a423-38ad02ae159e',
@@ -31,34 +35,191 @@ Ideal for embedded arrays of objects, e.g. `tags` and `backlinks` in the followi
3135
}
3236
```
3337

34-
The child must be an iterator component (like `<Datagrid>` or `<SingleFieldList>`).
38+
Leverage `<ArrayField>` e.g. in a Show view, to display the `tags` as a `<SingleFieldList>` and the `backlinks` as a `<Datagrid>`:
39+
40+
```jsx
41+
import {
42+
ArrayField,
43+
ChipField,
44+
Datagrid,
45+
Show,
46+
SimpleShowLayout,
47+
SingleFieldList,
48+
TextField
49+
} from 'react-admin';
50+
51+
const PostShow = () => (
52+
<Show>
53+
<SimpleShowLayout>
54+
<TextField source="title" />
55+
<ArrayField source="tags">
56+
<SingleFieldList>
57+
<ChipField source="name" size="small" />
58+
</SingleFieldList>
59+
</ArrayField>
60+
<ArrayField source="backlinks">
61+
<Datagrid bulkActionButtons={false}>
62+
<TextField source="uuid" />
63+
<TextField source="date" />
64+
<TextField source="url" />
65+
</Datagrid>
66+
</ArrayField>
67+
</SimpleShowLayout>
68+
</Show>
69+
)
70+
```
71+
72+
## Props
73+
74+
| Prop | Required | Type | Default | Description |
75+
|------------|----------|-------------------|---------|------------------------------------------|
76+
| `children` | Required | `ReactNode` | | The component to render the list. |
77+
| `filter` | Optional | `object` | | The filter to apply to the list. |
78+
| `perPage` | Optional | `number` | 1000 | The number of items to display per page. |
79+
| `sort` | Optional | `{ field, order}` | | The sort to apply to the list. |
80+
81+
`<ArrayField>` accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead).
82+
83+
`<ArrayField>` relies on [`useList`](./useList.md) to filter, paginate, and sort the data, so it accepts the same props.
84+
85+
## `children`
3586

36-
Here is how to display all the backlinks of the current post as a `<Datagrid>`:
87+
`<ArrayField>` renders its `children` component wrapped in a [`<ListContextProvider>`](./useListContext.md). Commonly used child components are [`<Datagrid>`](./Datagrid.md), [`<SingleFieldList>`](./SingleFieldList.md), and [`<SimpleList>`](./SimpleList.md).
3788

3889
```jsx
90+
{/* using SingleFieldList as child */}
91+
<ArrayField source="tags">
92+
<SingleFieldList>
93+
<ChipField source="name" />
94+
</SingleFieldList>
95+
</ArrayField>
96+
97+
{/* using Datagrid as child */}
3998
<ArrayField source="backlinks">
4099
<Datagrid>
41-
<DateField source="date" />
42-
<UrlField source="url" />
100+
<TextField source="uuid" />
101+
<TextField source="date" />
102+
<TextField source="url" />
43103
</Datagrid>
44104
</ArrayField>
105+
106+
{/* using SimpleList as child */}
107+
<ArrayField source="backlinks">
108+
<SimpleList
109+
primaryText={record => record.url}
110+
secondaryText={record => record.date}
111+
/>
112+
</ArrayField>
45113
```
46114

47-
And here is how to display all the tags of the current post as `<Chip>` components:
115+
## `filter`
116+
117+
You can use the `filter` prop to display only a subset of the items in the array. For instance, to display only the backlinks for a particular day:
48118

119+
{% raw %}
49120
```jsx
50-
<ArrayField source="tags">
121+
<ArrayField source="backlinks" filter={{ date: '2012-08-10T00:00:00.000Z' }}>
122+
<Datagrid>
123+
<TextField source="uuid" />
124+
<TextField source="date" />
125+
<TextField source="url" />
126+
</Datagrid>
127+
</ArrayField>
128+
```
129+
{% endraw %}
130+
131+
The filtering capabilities are very limited. For instance, there is no "greater than" or "less than" operator. You can only filter on the equality of a field.
132+
133+
## `perPage`
134+
135+
If the value is a large array, and you don't need to display all the items, you can use the `perPage` prop to limit the number of items displayed.
136+
137+
As `<ArrayField>` creates a [`ListContext`](./useListContext.md), you can use the `<Pagination>` component to navigate through the items.
138+
139+
```jsx
140+
import {
141+
ArrayField,
142+
Datagrid,
143+
Pagination,
144+
Show,
145+
SimpleShowLayout,
146+
TextField
147+
} from 'react-admin';
148+
149+
const PostShow = () => (
150+
<Show>
151+
<SimpleShowLayout>
152+
<TextField source="title" />
153+
<ArrayField source="backlinks" perPage={5}>
154+
<Datagrid>
155+
<TextField source="uuid" />
156+
<TextField source="date" />
157+
<TextField source="url" />
158+
</Datagrid>
159+
<Pagination />
160+
</ArrayField>
161+
</SimpleShowLayout>
162+
</Show>
163+
);
164+
```
165+
166+
## `sort`
167+
168+
By default, `<ArrayField>` displays the items in the order they are stored in the field. You can use the `sort` prop to change the sort order.
169+
170+
{% raw %}
171+
```jsx
172+
<ArrayField source="tags" sort={{ field: 'name', order: 'ASC' }}>
51173
<SingleFieldList>
52174
<ChipField source="name" />
53175
</SingleFieldList>
54176
</ArrayField>
55177
```
178+
{% endraw %}
56179

57-
## Properties
180+
## Using The List Context
58181

59-
`<ArrayField>` accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead).
182+
`<ArrayField>` creates a [`ListContext`](./useListContext.md) with the field value, so you can use any of the list context values in its children. This includes callbacks to sort, filter, and select items.
183+
184+
For instance, you can make the chips selectable as follows:
185+
186+
```jsx
187+
const SelectedChip = () => {
188+
const { selectedIds, onToggleItem } = useListContext();
189+
const record = useRecordContext();
190+
return (
191+
<ChipField
192+
source="title"
193+
clickable
194+
onClick={() => {
195+
onToggleItem(record.id);
196+
}}
197+
color={selectedIds.includes(record.id) ? 'primary' : 'default'}
198+
/>
199+
);
200+
};
201+
202+
const PostShow = () => (
203+
<Show>
204+
<SimpleShowLayout>
205+
<TextField source="title" />
206+
<ArrayField source="tags">
207+
<SingleFieldList>
208+
<SelectedChip />
209+
</SingleFieldList>
210+
</ArrayField>
211+
</SimpleShowLayout>
212+
</Show>
213+
)
214+
```
215+
216+
**Tip**: The selection logic uses the `id` field for each collection element, so the above example assumes that the `tags` field contains objects like `{ id: 123, name: 'bar' }`.
217+
218+
Check [the `useListContext` documentation](./useListContext.md) for more information on the list context values.
219+
220+
## Rendering An Array Of Strings
60221

61-
**Tip**: If you need to render a custom collection, it's often simpler to write your own component:
222+
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), it's often simpler to write your own component:
62223

63224
```jsx
64225
import { useRecordContext } from 'react-admin';

docs/img/array-field.webp

13.5 KB
Binary file not shown.

docs/useList.md

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ const { data, total } = useList({
103103
// data will be [{ id: 1, name: 'Arnold' }] and total will be 1
104104
```
105105

106+
The filtering capabilities are very limited. For instance, there is no "greater than" or "less than" operator. You can only filter on the equality of a field.
107+
106108
## `filterCallback`
107109

108110
Property for custom filter definition. Lets you apply local filters to the fetched data.

examples/simple/src/posts/PostList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ const PostList = () => {
163163
cellClassName="hiddenOnSmallScreens"
164164
headerClassName="hiddenOnSmallScreens"
165165
>
166-
<SingleFieldList>
166+
<SingleFieldList sx={{ my: -2 }}>
167167
<ChipField source="name.en" size="small" />
168168
</SingleFieldList>
169169
</ReferenceArrayField>

examples/simple/src/posts/PostShow.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ const PostShow = () => {
7272
sort={{ field: `name.${locale}`, order: 'ASC' }}
7373
>
7474
<SingleFieldList>
75-
<ChipField source={`name.${locale}`} />
75+
<ChipField
76+
source={`name.${locale}`}
77+
size="small"
78+
/>
7679
</SingleFieldList>
7780
</ReferenceArrayField>
7881
<DateField source="published_at" />

packages/ra-ui-materialui/src/field/ArrayField.spec.tsx

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { render } from '@testing-library/react';
2+
import { render, screen, waitFor } from '@testing-library/react';
33
import {
44
CoreAdminContext,
55
ResourceContextProvider,
@@ -12,6 +12,7 @@ import { NumberField } from './NumberField';
1212
import { TextField } from './TextField';
1313
import { Datagrid } from '../list';
1414
import { SimpleList } from '../list';
15+
import { ListContext } from './ArrayField.stories';
1516

1617
describe('<ArrayField />', () => {
1718
const sort = { field: 'id', order: 'ASC' };
@@ -130,4 +131,27 @@ describe('<ArrayField />', () => {
130131
expect(queryByText('baz')).not.toBeNull();
131132
expect(queryByText('456')).not.toBeNull();
132133
});
134+
135+
it('should create a ListContext with working callbacks', async () => {
136+
render(<ListContext />);
137+
screen.getByText('War and Peace');
138+
screen.getByText('Filter by title').click();
139+
await waitFor(() => {
140+
expect(screen.queryByText('War and Peace')).toBeNull();
141+
});
142+
const chip = screen.getByText('Resurrection');
143+
expect(
144+
(chip.parentNode as HTMLElement).className.includes(
145+
'MuiChip-colorDefault'
146+
)
147+
).toBeTruthy();
148+
chip.click();
149+
await waitFor(() => {
150+
expect(
151+
(chip.parentNode as HTMLElement).className.includes(
152+
'MuiChip-colorPrimary'
153+
)
154+
).toBeTruthy();
155+
});
156+
});
133157
});

0 commit comments

Comments
 (0)