Skip to content

Commit 55ee73b

Browse files
committed
Change WithListContext for standalone usage, add RecordsIterator.
1 parent 811ccbd commit 55ee73b

File tree

10 files changed

+344
-182
lines changed

10 files changed

+344
-182
lines changed

docs/ReferenceArrayFieldBase.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,18 @@ A typical `post` record therefore looks like this:
4545
In that case, use `<ReferenceArrayFieldBase>` to display the post tag names as a list of chips, as follows:
4646

4747
```jsx
48-
import { ListBase, ReferenceArrayFieldBase, RecordContextProvider } from 'react-admin';
48+
import { ListBase, ReferenceArrayFieldBase, RecordsIterator } from 'react-admin';
4949

5050
export const PostList = () => (
5151
<ListBase
5252
render={({ data, isPending }) => (
5353
<>
5454
{!isPending &&
55-
data.map(record => (
56-
<RecordContextProvider
57-
value={record}
58-
key={record.id}
59-
>
60-
<ReferenceArrayFieldBase reference="tags" source="tag_ids">
61-
<TagList />
62-
</ReferenceArrayFieldBase>
63-
</RecordContextProvider>
64-
))}
55+
<RecordsIterator data={data}>
56+
<ReferenceArrayFieldBase reference="tags" source="tag_ids">
57+
<TagList />
58+
</ReferenceArrayFieldBase>
59+
</RecordsIterator>
6560
</>
6661
)}
6762
/>

docs/ReferenceFieldBase.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,11 @@ export const PostList = () => (
212212
render={({ data, isPending }) => (
213213
<>
214214
{!isPending &&
215-
data.map(author => (
216-
<RecordContextProvider
217-
value={author}
218-
key={author.id}
219-
>
220-
<ReferenceFieldBase source="user_id" reference="users">
221-
<AuthorView />
222-
</ReferenceFieldBase>
223-
</RecordContextProvider>
224-
))}
215+
<RecordsIterator data={data}>
216+
<ReferenceFieldBase source="user_id" reference="users">
217+
<AuthorView />
218+
</ReferenceFieldBase>
219+
</RecordsIterator>
225220
</>
226221
)}
227222
/>
@@ -305,4 +300,4 @@ React-Admin will call `canAccess` with the following parameters:
305300
- If the `users` resource has a Show view: `{ action: "show", resource: 'posts', record: Object }`
306301
- If the `users` resource has an Edit view: `{ action: "edit", resource: 'posts', record: Object }`
307302
308-
And the link property of the referenceField context will be set accordingly. It will be set to false if the access is denied.
303+
And the link property of the referenceField context will be set accordingly. It will be set to false if the access is denied.

packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as React from 'react';
2-
import { RecordContextProvider } from 'ra-core';
2+
import { RecordsIterator } from 'ra-core';
33
import { QueryClient } from '@tanstack/react-query';
44
import { CoreAdmin } from '../../core/CoreAdmin';
55
import { Resource } from '../../core/Resource';
66
import { ShowBase } from '../../controller/show/ShowBase';
77
import { TestMemoryRouter } from '../../routing';
88
import { ReferenceManyFieldBase } from './ReferenceManyFieldBase';
9-
import { ListBase, useListContext, WithListContext } from '../list';
9+
import { ListBase, useListContext } from '../list';
1010
import fakeRestDataProvider from 'ra-data-fakerest';
1111

1212
export default {
@@ -133,34 +133,31 @@ export const InAList = ({ dataProvider = dataProviderWithAuthorList }) => (
133133
<Resource
134134
name="authors"
135135
list={
136-
<ListBase>
137-
<WithListContext
138-
render={({ data, isPending }) => (
139-
<>
140-
{!isPending &&
141-
data.map(author => (
142-
<RecordContextProvider
143-
value={author}
144-
key={author.id}
145-
>
146-
<div>
147-
<h3>
148-
{author.last_name} Books
149-
</h3>
150-
<ReferenceManyFieldBase
151-
target="author"
152-
source="id"
153-
reference="books"
154-
>
155-
<AuthorList source="title" />
156-
</ReferenceManyFieldBase>
157-
</div>
158-
</RecordContextProvider>
159-
))}
160-
</>
161-
)}
162-
/>
163-
</ListBase>
136+
<ListBase
137+
render={({ data, isPending }) => (
138+
<>
139+
{!isPending && (
140+
<RecordsIterator
141+
data={data}
142+
render={author => (
143+
<div>
144+
<h3>
145+
{author.last_name} Books
146+
</h3>
147+
<ReferenceManyFieldBase
148+
target="author"
149+
source="id"
150+
reference="books"
151+
>
152+
<AuthorList source="title" />
153+
</ReferenceManyFieldBase>
154+
</div>
155+
)}
156+
/>
157+
)}
158+
</>
159+
)}
160+
/>
164161
}
165162
/>
166163
</CoreAdmin>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { UsingChildren, UsingRender } from './RecordsIterator.stories';
4+
5+
describe('<RecordsIterator>', () => {
6+
describe.each([
7+
{ Story: UsingRender, prop: 'render' },
8+
{ Story: UsingChildren, prop: 'children' },
9+
])('Using the $prop prop', ({ Story }) => {
10+
it('should render the records', async () => {
11+
render(<Story />);
12+
13+
await screen.findByText('War and Peace');
14+
await screen.findByText('The Lion, the Witch and the Wardrobe');
15+
});
16+
it('should render the pending prop when ListContext.isPending is true', async () => {
17+
render(<Story isPending />);
18+
19+
await screen.findByText('Loading...');
20+
});
21+
it('should render the empty prop when there is no data', async () => {
22+
render(<Story empty />);
23+
24+
await screen.findByText('No data');
25+
});
26+
});
27+
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React from 'react';
2+
import { useList, UseListOptions } from './useList';
3+
import { ListContextProvider } from './ListContextProvider';
4+
import { RecordsIterator } from './RecordsIterator';
5+
import { useRecordContext } from '../record';
6+
import { WithListContext } from './WithListContext';
7+
8+
export default {
9+
title: 'ra-core/controller/list/RecordsIterator',
10+
};
11+
12+
const data = [
13+
{ id: 1, title: 'War and Peace' },
14+
{ id: 2, title: 'The Little Prince' },
15+
{ id: 3, title: "Swann's Way" },
16+
{ id: 4, title: 'A Tale of Two Cities' },
17+
{ id: 5, title: 'The Lord of the Rings' },
18+
{ id: 6, title: 'And Then There Were None' },
19+
{ id: 7, title: 'Dream of the Red Chamber' },
20+
{ id: 8, title: 'The Hobbit' },
21+
{ id: 9, title: 'She: A History of Adventure' },
22+
{ id: 10, title: 'The Lion, the Witch and the Wardrobe' },
23+
];
24+
25+
export const UsingRender = ({
26+
empty,
27+
...props
28+
}: UseListOptions & { empty?: boolean }) => {
29+
const value = useList({
30+
data: empty ? [] : data,
31+
sort: { field: 'id', order: 'ASC' },
32+
...props,
33+
});
34+
35+
return (
36+
<ListContextProvider value={value}>
37+
<WithListContext
38+
loading={<div>Loading...</div>}
39+
empty={<div>No data</div>}
40+
render={() => (
41+
<ul
42+
style={{
43+
listStyleType: 'none',
44+
}}
45+
>
46+
<RecordsIterator
47+
render={record => (
48+
<li
49+
style={{
50+
padding: '10px',
51+
border: '1px solid #ccc',
52+
}}
53+
>
54+
{record.title}
55+
</li>
56+
)}
57+
/>
58+
</ul>
59+
)}
60+
/>
61+
</ListContextProvider>
62+
);
63+
};
64+
65+
UsingRender.args = {
66+
isPending: false,
67+
empty: false,
68+
};
69+
70+
UsingRender.argTypes = {
71+
isPending: { control: 'boolean' },
72+
empty: { control: 'boolean' },
73+
};
74+
75+
const ListItem = () => {
76+
const record = useRecordContext();
77+
return (
78+
<li
79+
style={{
80+
padding: '10px',
81+
border: '1px solid #ccc',
82+
}}
83+
>
84+
{record?.title}
85+
</li>
86+
);
87+
};
88+
89+
export const UsingChildren = ({
90+
empty,
91+
...props
92+
}: UseListOptions & { empty?: boolean }) => {
93+
const value = useList({
94+
data: empty ? [] : data,
95+
sort: { field: 'id', order: 'ASC' },
96+
...props,
97+
});
98+
99+
return (
100+
<ListContextProvider value={value}>
101+
<WithListContext
102+
loading={<div>Loading...</div>}
103+
empty={<div>No data</div>}
104+
render={() => (
105+
<ul
106+
style={{
107+
listStyleType: 'none',
108+
}}
109+
>
110+
<RecordsIterator>
111+
<ListItem />
112+
</RecordsIterator>
113+
</ul>
114+
)}
115+
/>
116+
</ListContextProvider>
117+
);
118+
};
119+
120+
UsingChildren.args = {
121+
isPending: false,
122+
empty: false,
123+
};
124+
125+
UsingChildren.argTypes = {
126+
isPending: { control: 'boolean' },
127+
empty: { control: 'boolean' },
128+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
import { RaRecord } from '../../types';
3+
import { useListContextWithProps } from './useListContextWithProps';
4+
import { RecordContextProvider } from '../record';
5+
import { ListControllerSuccessResult } from './useListController';
6+
7+
export const RecordsIterator = <RecordType extends RaRecord = any>(
8+
props: RecordsIteratorProps<RecordType>
9+
) => {
10+
const { children, render } = props;
11+
const { data, total, isPending, error } =
12+
useListContextWithProps<RecordType>(props);
13+
14+
if (
15+
isPending === true ||
16+
error ||
17+
data == null ||
18+
data.length === 0 ||
19+
total === 0
20+
) {
21+
console.warn(
22+
'<RecordsIterator> does not handle loading, empty and error states. Use <WithListContext>.'
23+
);
24+
return null;
25+
}
26+
27+
if (!render && !children) {
28+
return null;
29+
}
30+
31+
return (
32+
<>
33+
{data.map((record, index) => (
34+
<RecordContextProvider
35+
key={record.id ?? `row${index}`}
36+
value={record}
37+
>
38+
{render ? render(record, index) : children}
39+
</RecordContextProvider>
40+
))}
41+
</>
42+
);
43+
};
44+
45+
export interface RecordsIteratorProps<RecordType extends RaRecord = any>
46+
extends Partial<ListControllerSuccessResult<RecordType>> {
47+
children?: React.ReactNode;
48+
render?: (record: RecordType, index: number) => React.ReactNode;
49+
}

packages/ra-core/src/controller/list/WithListContext.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { ReactElement } from 'react';
22
import { RaRecord } from '../../types';
33
import { ListControllerResult } from './useListController';
4-
import { useListContext } from './useListContext';
4+
import { useListContextWithProps } from './useListContextWithProps';
55

66
/**
7-
* Render prop version of useListContext
7+
* Render prop version of useListContextWithProps
88
*
99
* @example
1010
* const BookList = () => (
@@ -24,8 +24,9 @@ export const WithListContext = <RecordType extends RaRecord>({
2424
loading,
2525
error: errorElement,
2626
render,
27+
...props
2728
}: WithListContextProps<RecordType>) => {
28-
const context = useListContext<RecordType>();
29+
const context = useListContextWithProps<RecordType>(props);
2930
const { data, total, isPending, error } = context;
3031

3132
if (isPending === true && loading) {
@@ -43,17 +44,16 @@ export const WithListContext = <RecordType extends RaRecord>({
4344
return render(context) || null;
4445
};
4546

46-
export interface WithListContextProps<RecordType extends RaRecord> {
47+
export type WithListContextProps<RecordType extends RaRecord> = Partial<
48+
Pick<
49+
ListControllerResult<RecordType>,
50+
'data' | 'total' | 'isPending' | 'error'
51+
>
52+
> & {
4753
render: (
48-
context: ListControllerResult<RecordType>
54+
context: Partial<ListControllerResult<RecordType>>
4955
) => ReactElement | false | null;
50-
label?: string;
51-
empty?: React.ReactElement;
52-
loading?: React.ReactElement;
53-
error?: React.ReactElement;
54-
}
55-
56-
/**
57-
* @deprecated use WithListContext instead
58-
*/
59-
export const ListIterator = WithListContext;
56+
empty?: React.ReactNode;
57+
loading?: React.ReactNode;
58+
error?: React.ReactNode;
59+
};

0 commit comments

Comments
 (0)