Skip to content

Commit 297599d

Browse files
authored
Merge pull request #11191 from marmelab/array-field-base
Add ArrayFieldBase component
2 parents 1990604 + 8a4ab01 commit 297599d

6 files changed

Lines changed: 247 additions & 93 deletions

File tree

docs/ArrayField.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ storybook_path: ra-ui-materialui-fields-arrayfield--basic
1212

1313
`<ArrayField>` creates a [`ListContext`](./useListContext.md) with the field value, and renders its children components - usually iterator components like [`<DataTable>`](./DataTable.md) or [`<SingleFieldList>`](./SingleFieldList.md).
1414

15+
`<ArrayField>` is the Material UI export of the headless [`<ArrayFieldBase>`](./ArrayFieldBase.md) component from `ra-core`.
16+
1517
## Usage
1618

1719
`<ArrayField>` is ideal for collections of objects, e.g. `tags` and `backlinks` in the following `post` object:

docs/ArrayFieldBase.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
---
2+
layout: default
3+
title: "The ArrayFieldBase Component"
4+
---
5+
6+
# `<ArrayFieldBase>`
7+
8+
`<ArrayFieldBase>` renders an embedded array of objects.
9+
`<ArrayFieldBase>` is a headless component, handling only the list logic. This allows you to use any UI library for the render. For a Material UI version, see [`<ArrayField>`](./ArrayField.md).
10+
11+
`<ArrayFieldBase>` creates a [`ListContext`](./useListContext.md) with the field value, and renders its children components.
12+
13+
## Usage
14+
15+
`<ArrayFieldBase>` is ideal for collections of objects, e.g. `tags` and `backlinks` in the following `post` object:
16+
17+
```js
18+
{
19+
id: 123,
20+
title: 'Lorem Ipsum Sit Amet',
21+
tags: [{ name: 'dolor' }, { name: 'sit' }, { name: 'amet' }],
22+
backlinks: [
23+
{
24+
uuid: '34fdf393-f449-4b04-a423-38ad02ae159e',
25+
date: '2012-08-10T00:00:00.000Z',
26+
url: 'https://example.com/foo/bar.html',
27+
},
28+
{
29+
uuid: 'd907743a-253d-4ec1-8329-404d4c5e6cf1',
30+
date: '2012-08-14T00:00:00.000Z',
31+
url: 'https://blog.johndoe.com/2012/08/12/foobar.html',
32+
}
33+
]
34+
}
35+
```
36+
37+
You can leverage `<ArrayFieldBase>` in a Show view and render the list using any component reading the list context:
38+
39+
{% raw %}
40+
```jsx
41+
import {
42+
ArrayFieldBase,
43+
RecordsIterator,
44+
Show,
45+
SimpleShowLayout,
46+
TextField,
47+
} from 'react-admin';
48+
49+
const PostShow = () => (
50+
<Show>
51+
<SimpleShowLayout>
52+
<TextField source="title" />
53+
<ArrayFieldBase source="tags">
54+
<ul>
55+
<RecordsIterator render={tag => <li>{tag.name}</li>} />
56+
</ul>
57+
</ArrayFieldBase>
58+
<ArrayFieldBase source="backlinks">
59+
<ul>
60+
<RecordsIterator
61+
render={backlink => (
62+
<li>
63+
<a href={backlink.url}>{backlink.url}</a>
64+
</li>
65+
)}
66+
/>
67+
</ul>
68+
</ArrayFieldBase>
69+
</SimpleShowLayout>
70+
</Show>
71+
);
72+
```
73+
{% endraw %}
74+
75+
## Props
76+
77+
| Prop | Required | Type | Default | Description |
78+
|------------|----------|-------------------|---------|------------------------------------------|
79+
| `children` | Required | `ReactNode` | | The component to render the list. |
80+
| `filter` | Optional | `object` | | The filter to apply to the list. |
81+
| `exporter` | Optional | `function` | `default Exporter` | The function called by export buttons in the list context. |
82+
| `perPage` | Optional | `number` | 1000 | The number of items to display per page. |
83+
| `sort` | Optional | `{ field, order}` | | The sort to apply to the list. |
84+
85+
`<ArrayFieldBase>` accepts the base field props `source`, `record`, and `resource`.
86+
87+
`<ArrayFieldBase>` relies on [`useList`](./useList.md) to filter, paginate, and sort the data, so it accepts the same props.
88+
89+
## `children`
90+
91+
`<ArrayFieldBase>` renders its `children` wrapped in a [`<ListContextProvider>`](./useListContext.md). Commonly used children are [`<RecordsIterator>`](./RecordsIterator.md), [`<WithListContext>`](./WithListContext.md), or any custom component using `useListContext()`.
92+
93+
{% raw %}
94+
```jsx
95+
import { ArrayFieldBase, WithListContext } from 'react-admin';
96+
97+
const BacklinksField = () => (
98+
<ArrayFieldBase source="backlinks">
99+
<WithListContext
100+
render={({ data }) => (
101+
<ul>
102+
{data?.map(backlink => (
103+
<li key={backlink.uuid}>{backlink.url}</li>
104+
))}
105+
</ul>
106+
)}
107+
/>
108+
</ArrayFieldBase>
109+
);
110+
```
111+
{% endraw %}
112+
113+
## `filter`
114+
115+
By default, `<ArrayFieldBase>` displays all the records in the embedded array. Use the `filter` prop to restrict them.
116+
117+
{% raw %}
118+
```jsx
119+
<ArrayFieldBase
120+
source="backlinks"
121+
filter={{ date: '2012-08-10T00:00:00.000Z' }}
122+
>
123+
<WithListContext
124+
render={({ data }) => (
125+
<ul>
126+
{data?.map(backlink => (
127+
<li key={backlink.uuid}>{backlink.url}</li>
128+
))}
129+
</ul>
130+
)}
131+
/>
132+
</ArrayFieldBase>
133+
```
134+
{% endraw %}
135+
136+
## `perPage`
137+
138+
Because `<ArrayFieldBase>` creates a [`ListContext`](./useListContext.md), you can paginate the embedded array with any pagination UI wired to that context.
139+
140+
## `sort`
141+
142+
By default, `<ArrayFieldBase>` displays the items in the order they are stored in the field. You can use the `sort` prop to change the sort order.
143+
144+
{% raw %}
145+
```jsx
146+
<ArrayFieldBase source="tags" sort={{ field: 'name', order: 'ASC' }}>
147+
<ul>
148+
<RecordsIterator render={tag => <li>{tag.name}</li>} />
149+
</ul>
150+
</ArrayFieldBase>
151+
```
152+
{% endraw %}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as React from 'react';
2+
import { type ReactNode } from 'react';
3+
4+
import type { Exporter, FilterPayload, SortPayload } from '../../types';
5+
import { genericMemo } from '../../util/genericMemo';
6+
import { useFieldValue } from '../../util/useFieldValue';
7+
import { ListContextProvider, useList } from '../list';
8+
import type { BaseFieldProps } from './types';
9+
10+
/**
11+
* Renders an embedded array of objects.
12+
*
13+
* ArrayFieldBase creates a ListContext with the field value, and renders its
14+
* children components - usually iterator components.
15+
*
16+
* @example
17+
* const PostShow = () => (
18+
* <ShowBase>
19+
* <ArrayFieldBase source="tags">
20+
* <ul>
21+
* <RecordsIterator
22+
* render={record => <li key={record.id}>{record.name}</li>}
23+
* />
24+
* </ul>
25+
* </ArrayFieldBase>
26+
* </ShowBase>
27+
* );
28+
*
29+
* @see useListContext
30+
*/
31+
const ArrayFieldBaseImpl = <
32+
RecordType extends Record<string, any> = Record<string, any>,
33+
>(
34+
props: ArrayFieldBaseProps<RecordType>
35+
) => {
36+
const { children, resource, perPage, sort, filter, exporter } = props;
37+
const data = useFieldValue(props) || emptyArray;
38+
const listContext = useList({
39+
data,
40+
resource,
41+
perPage,
42+
sort,
43+
filter,
44+
exporter,
45+
});
46+
47+
return (
48+
<ListContextProvider value={listContext}>
49+
{children}
50+
</ListContextProvider>
51+
);
52+
};
53+
54+
ArrayFieldBaseImpl.displayName = 'ArrayFieldBaseImpl';
55+
56+
export const ArrayFieldBase = genericMemo(ArrayFieldBaseImpl);
57+
58+
export interface ArrayFieldBaseProps<
59+
RecordType extends Record<string, any> = Record<string, any>,
60+
> extends BaseFieldProps<RecordType> {
61+
children?: ReactNode;
62+
perPage?: number;
63+
sort?: SortPayload;
64+
filter?: FilterPayload;
65+
exporter?: Exporter<any> | false;
66+
}
67+
68+
const emptyArray = [];

packages/ra-core/src/controller/field/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './ArrayFieldBase';
12
export * from './ReferenceOneFieldBase';
23
export * from './ReferenceFieldBase';
34
export * from './ReferenceFieldContext';

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TestMemoryRouter,
99
ResourceContextProvider,
1010
downloadCSV,
11+
RecordsIterator,
1112
} from 'ra-core';
1213
import polyglotI18nProvider from 'ra-i18n-polyglot';
1314
import englishMessages from 'ra-language-english';
@@ -42,6 +43,18 @@ export const Basic = () => (
4243
</TestMemoryRouter>
4344
);
4445

46+
export const WithRecordsIterator = () => (
47+
<TestMemoryRouter>
48+
<ArrayField record={{ id: 123, books }} source="books">
49+
<ul>
50+
<RecordsIterator
51+
render={record => <li key={record.id}>{record.title}</li>}
52+
/>
53+
</ul>
54+
</ArrayField>
55+
</TestMemoryRouter>
56+
);
57+
4558
const i18nProvider = polyglotI18nProvider(() => englishMessages);
4659

4760
export const PerPage = () => (
Lines changed: 11 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,31 @@
1-
import * as React from 'react';
2-
import { ReactNode } from 'react';
3-
import {
4-
Exporter,
5-
ListContextProvider,
6-
useList,
7-
SortPayload,
8-
FilterPayload,
9-
useFieldValue,
10-
genericMemo,
11-
} from 'ra-core';
1+
import { ArrayFieldBase, type ArrayFieldBaseProps } from 'ra-core';
122

13-
import { FieldProps } from './types';
3+
import type { FieldProps } from './types';
144

155
/**
166
* Renders an embedded array of objects.
177
*
18-
* ArrayField creates a ListContext with the field value, and renders its children components -
19-
* usually iterator components like Datagrid, SingleFieldList, or SimpleList.
8+
* ArrayField creates a ListContext with the field value, and renders its
9+
* children components - usually iterator components.
2010
*
21-
* @example // Display all the tags of the current post as `<Chip>` components
22-
* // const post = {
23-
* // id: 123
24-
* // tags: [
25-
* // { name: 'foo' },
26-
* // { name: 'bar' }
27-
* // ]
28-
* // };
11+
* @example
2912
* const PostShow = () => (
3013
* <Show>
3114
* <SimpleShowLayout>
3215
* <ArrayField source="tags">
33-
* <SingleFieldList>
34-
* <ChipField source="name" />
35-
* </SingleFieldList>
16+
* <SingleFieldList>
17+
* <ChipField source="name" />
18+
* </SingleFieldList>
3619
* </ArrayField>
3720
* </SimpleShowLayout>
3821
* </Show>
3922
* );
4023
*
41-
* @example // Display all the backlinks of the current post as a `<Datagrid>`
42-
* // const post = {
43-
* // id: 123
44-
* // backlinks: [
45-
* // {
46-
* // uuid: '34fdf393-f449-4b04-a423-38ad02ae159e',
47-
* // date: '2012-08-10T00:00:00.000Z',
48-
* // url: 'http://example.com/foo/bar.html',
49-
* // },
50-
* // {
51-
* // uuid: 'd907743a-253d-4ec1-8329-404d4c5e6cf1',
52-
* // date: '2012-08-14T00:00:00.000Z',
53-
* // url: 'https://blog.johndoe.com/2012/08/12/foobar.html',
54-
* // }
55-
* // ]
56-
* // };
57-
* <ArrayField source="backlinks">
58-
* <Datagrid>
59-
* <DateField source="date" />
60-
* <UrlField source="url" />
61-
* </Datagrid>
62-
* </ArrayField>
63-
*
64-
* @example // If you need to render a collection of strings, it's often simpler to write your own component
65-
* const TagsField = () => {
66-
* const record = useRecordContext();
67-
* return (
68-
* <ul>
69-
* {record.tags.map(item => (
70-
* <li key={item.name}>{item.name}</li>
71-
* ))}
72-
* </ul>
73-
* );
74-
* };
75-
*
7624
* @see useListContext
7725
*/
78-
const ArrayFieldImpl = <
79-
RecordType extends Record<string, any> = Record<string, any>,
80-
>(
81-
props: ArrayFieldProps<RecordType>
82-
) => {
83-
const { children, resource, perPage, sort, filter, exporter } = props;
84-
const data = useFieldValue(props) || emptyArray;
85-
const listContext = useList({
86-
data,
87-
resource,
88-
perPage,
89-
sort,
90-
filter,
91-
exporter,
92-
});
93-
return (
94-
<ListContextProvider value={listContext}>
95-
{children}
96-
</ListContextProvider>
97-
);
98-
};
99-
ArrayFieldImpl.displayName = 'ArrayFieldImpl';
100-
101-
export const ArrayField = genericMemo(ArrayFieldImpl);
26+
export const ArrayField = ArrayFieldBase;
10227

10328
export interface ArrayFieldProps<
10429
RecordType extends Record<string, any> = Record<string, any>,
105-
> extends FieldProps<RecordType> {
106-
children?: ReactNode;
107-
perPage?: number;
108-
sort?: SortPayload;
109-
filter?: FilterPayload;
110-
exporter?: Exporter<any> | false;
111-
}
112-
113-
const emptyArray = [];
30+
> extends ArrayFieldBaseProps<RecordType>,
31+
FieldProps<RecordType> {}

0 commit comments

Comments
 (0)