Skip to content

Commit fb15d48

Browse files
committed
Introduce <ListIterator>
1 parent ccf10db commit fb15d48

File tree

6 files changed

+248
-35
lines changed

6 files changed

+248
-35
lines changed
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 './ListIterator.stories';
4+
5+
describe('<ListIterator>', () => {
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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React from 'react';
2+
import { useList, UseListOptions } from './useList';
3+
import { ListContextProvider } from './ListContextProvider';
4+
import { ListIterator } from './ListIterator';
5+
import { useRecordContext } from '../record';
6+
7+
export default {
8+
title: 'ra-core/controller/list/ListIterator',
9+
};
10+
11+
const data = [
12+
{ id: 1, title: 'War and Peace' },
13+
{ id: 2, title: 'The Little Prince' },
14+
{ id: 3, title: "Swann's Way" },
15+
{ id: 4, title: 'A Tale of Two Cities' },
16+
{ id: 5, title: 'The Lord of the Rings' },
17+
{ id: 6, title: 'And Then There Were None' },
18+
{ id: 7, title: 'Dream of the Red Chamber' },
19+
{ id: 8, title: 'The Hobbit' },
20+
{ id: 9, title: 'She: A History of Adventure' },
21+
{ id: 10, title: 'The Lion, the Witch and the Wardrobe' },
22+
];
23+
24+
export const UsingRender = ({
25+
empty,
26+
...props
27+
}: UseListOptions & { empty?: boolean }) => {
28+
const value = useList({
29+
data: empty ? [] : data,
30+
sort: { field: 'id', order: 'ASC' },
31+
...props,
32+
});
33+
34+
return (
35+
<ListContextProvider value={value}>
36+
<ul
37+
style={{
38+
listStyleType: 'none',
39+
}}
40+
>
41+
<ListIterator
42+
pending={<div>Loading...</div>}
43+
empty={<div>No data</div>}
44+
render={record => (
45+
<li
46+
style={{
47+
padding: '10px',
48+
border: '1px solid #ccc',
49+
}}
50+
>
51+
{record.title}
52+
</li>
53+
)}
54+
/>
55+
</ul>
56+
</ListContextProvider>
57+
);
58+
};
59+
60+
UsingRender.args = {
61+
isPending: false,
62+
empty: false,
63+
};
64+
65+
UsingRender.argTypes = {
66+
isPending: { control: 'boolean' },
67+
empty: { control: 'boolean' },
68+
};
69+
70+
const ListItem = () => {
71+
const record = useRecordContext();
72+
return (
73+
<li
74+
style={{
75+
padding: '10px',
76+
border: '1px solid #ccc',
77+
}}
78+
>
79+
{record?.title}
80+
</li>
81+
);
82+
};
83+
84+
export const UsingChildren = ({
85+
empty,
86+
...props
87+
}: UseListOptions & { empty?: boolean }) => {
88+
const value = useList({
89+
data: empty ? [] : data,
90+
sort: { field: 'id', order: 'ASC' },
91+
...props,
92+
});
93+
94+
return (
95+
<ListContextProvider value={value}>
96+
<ul
97+
style={{
98+
listStyleType: 'none',
99+
}}
100+
>
101+
<ListIterator
102+
pending={<div>Loading...</div>}
103+
empty={<div>No data</div>}
104+
>
105+
<ListItem />
106+
</ListIterator>
107+
</ul>
108+
</ListContextProvider>
109+
);
110+
};
111+
112+
UsingChildren.args = {
113+
isPending: false,
114+
empty: false,
115+
};
116+
117+
UsingChildren.argTypes = {
118+
isPending: { control: 'boolean' },
119+
empty: { control: 'boolean' },
120+
};
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 { RaRecord } from '../../types';
3+
import { useListContextWithProps } from './useListContextWithProps';
4+
import { RecordContextProvider } from '../record';
5+
6+
export const ListIterator = <RecordType extends RaRecord = any>(
7+
props: ListIteratorProps<RecordType>
8+
) => {
9+
const { children, empty = null, pending = null, render } = props;
10+
const { data, total, isPending } =
11+
useListContextWithProps<RecordType>(props);
12+
13+
if (isPending === true) {
14+
return pending ? pending : null;
15+
}
16+
17+
if (data == null || data.length === 0 || total === 0) {
18+
return empty ? empty : null;
19+
}
20+
21+
if (!render && !children) {
22+
throw new Error(
23+
'<ListIterator>: either `render` or `children` prop must be provided'
24+
);
25+
}
26+
27+
if (render) {
28+
return (
29+
<>
30+
{data.map((record, index) => (
31+
<RecordContextProvider
32+
key={record.id ?? `row${index}`}
33+
value={record}
34+
>
35+
{render(record, index)}
36+
</RecordContextProvider>
37+
))}
38+
</>
39+
);
40+
}
41+
42+
return (
43+
<>
44+
{data.map((record, index) => (
45+
<RecordContextProvider
46+
key={record.id ?? `row${index}`}
47+
value={record}
48+
>
49+
{children}
50+
</RecordContextProvider>
51+
))}
52+
</>
53+
);
54+
};
55+
56+
export interface ListIteratorProps<RecordType extends RaRecord = any> {
57+
children?: React.ReactNode;
58+
empty?: React.ReactElement;
59+
pending?: React.ReactElement;
60+
render?: (record: RecordType, index: number) => React.ReactNode;
61+
// can be injected when using the component without context
62+
resource?: string;
63+
data?: RecordType[];
64+
total?: number;
65+
isLoading?: boolean;
66+
isPending?: boolean;
67+
isLoaded?: boolean;
68+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './ListContext';
55
export * from './ListContextProvider';
66
export * from './ListController';
77
export * from './ListFilterContext';
8+
export * from './ListIterator';
89
export * from './ListPaginationContext';
910
export * from './ListSortContext';
1011
export * from './queryReducer';

packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
useThemeProps,
1414
} from '@mui/material/styles';
1515
import {
16+
ListIterator,
1617
type RaRecord,
17-
RecordContextProvider,
1818
sanitizeListRestProps,
1919
useGetRecordRepresentation,
2020
useListContextWithProps,
@@ -122,8 +122,10 @@ export const SimpleList = <RecordType extends RaRecord = any>(
122122

123123
return (
124124
<Root className={className} {...sanitizeListRestProps(rest)}>
125-
{data.map((record, rowIndex) => (
126-
<RecordContextProvider key={record.id} value={record}>
125+
<ListIterator<RecordType>
126+
data={data}
127+
total={total}
128+
render={(record, rowIndex) => (
127129
<SimpleListItem
128130
key={record.id}
129131
rowIndex={rowIndex}
@@ -144,8 +146,8 @@ export const SimpleList = <RecordType extends RaRecord = any>(
144146
rowIndex={rowIndex}
145147
/>
146148
</SimpleListItem>
147-
</RecordContextProvider>
148-
))}
149+
)}
150+
/>
149151
</Root>
150152
);
151153
};

packages/ra-ui-materialui/src/list/SingleFieldList.tsx

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {
1212
useListContextWithProps,
1313
useResourceContext,
1414
type RaRecord,
15-
RecordContextProvider,
1615
RecordRepresentation,
1716
useCreatePath,
17+
ListIterator,
1818
} from 'ra-core';
1919

2020
import { LinearProgress } from '../layout/LinearProgress';
@@ -54,7 +54,9 @@ import { Link } from '../Link';
5454
* </SingleFieldList>
5555
* </ReferenceManyField>
5656
*/
57-
export const SingleFieldList = (inProps: SingleFieldListProps) => {
57+
export const SingleFieldList = <RecordType extends RaRecord = any>(
58+
inProps: SingleFieldListProps<RecordType>
59+
) => {
5860
const props = useThemeProps({
5961
props: inProps,
6062
name: PREFIX,
@@ -87,21 +89,21 @@ export const SingleFieldList = (inProps: SingleFieldListProps) => {
8789
className={className}
8890
{...sanitizeListRestProps(rest)}
8991
>
90-
{data.map((record, rowIndex) => {
91-
const resourceLinkPath = !linkType
92-
? false
93-
: createPath({
94-
resource,
95-
type: linkType,
96-
id: record.id,
97-
});
98-
99-
if (resourceLinkPath) {
100-
return (
101-
<RecordContextProvider
102-
value={record}
103-
key={record.id ?? `row${rowIndex}`}
104-
>
92+
<ListIterator<RecordType>
93+
data={data}
94+
total={total}
95+
isPending={isPending}
96+
render={record => {
97+
const resourceLinkPath = !linkType
98+
? false
99+
: createPath({
100+
resource,
101+
type: linkType,
102+
id: record.id,
103+
});
104+
105+
if (resourceLinkPath) {
106+
return (
105107
<Link
106108
className={SingleFieldListClasses.link}
107109
to={resourceLinkPath}
@@ -111,19 +113,12 @@ export const SingleFieldList = (inProps: SingleFieldListProps) => {
111113
<DefaultChildComponent clickable />
112114
)}
113115
</Link>
114-
</RecordContextProvider>
115-
);
116-
}
117-
118-
return (
119-
<RecordContextProvider
120-
value={record}
121-
key={record.id ?? `row${rowIndex}`}
122-
>
123-
{children || <DefaultChildComponent />}
124-
</RecordContextProvider>
125-
);
126-
})}
116+
);
117+
}
118+
119+
return children || <DefaultChildComponent />;
120+
}}
121+
/>
127122
</Root>
128123
);
129124
};

0 commit comments

Comments
 (0)