Skip to content

Commit 0e1a1ce

Browse files
authored
Merge pull request #8781 from marmelab/infinite-list
Infinite list
2 parents ab9fe9c + 1a25332 commit 0e1a1ce

35 files changed

Lines changed: 2402 additions & 74 deletions

cypress/e2e/mobile.cy.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import listPageFactory from '../support/ListPage';
2+
3+
describe('Mobile UI', () => {
4+
const ListPagePosts = listPageFactory('/#/posts');
5+
6+
beforeEach(() => {
7+
window.localStorage.clear();
8+
cy.viewport('iphone-x');
9+
});
10+
11+
describe('Infinite Scroll', () => {
12+
it.only('should load more items when scrolling to the bottom of the page', () => {
13+
ListPagePosts.navigate();
14+
cy.contains('Sed quo et et fugiat modi').should('not.exist');
15+
cy.scrollTo('bottom');
16+
cy.wait(500);
17+
cy.scrollTo('bottom');
18+
cy.contains('Sed quo et et fugiat modi');
19+
});
20+
});
21+
});

docs/InfiniteList.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
layout: default
3+
title: "The InfiniteList Component"
4+
---
5+
6+
# `<InfiniteList>`
7+
8+
The `<InfiniteList>` component is an alternative to [the `<List>` component](./List.md) that allows user to load more records when they scroll to the bottom of the list. It's useful when you have a large number of records, or when users are using a mobile device.
9+
10+
<video controls autoplay muted loop width="100%">
11+
<source src="./img/infinite-book-list.webm" poster="./img/infinite-book-list.webp" type="video/webm">
12+
Your browser does not support the video tag.
13+
</video>
14+
15+
`<InfiniteList>` fetches the list of records from the data provider, and renders the default list layout (title, buttons, filters). It delegates the rendering of the list of records to its child component. Usually, it's a [`<Datagrid>`](./Datagrid.md) or a [`<SimpleList>`](./SimpleList.md), responsible for displaying a table with one row for each record.
16+
17+
## Usage
18+
19+
Here is the minimal code necessary to display a list of books with infinite scroll:
20+
21+
```jsx
22+
// in src/books.js
23+
import { InfiniteList, Datagrid, TextField, DateField } from 'react-admin';
24+
25+
export const BookList = () => (
26+
<InfiniteList>
27+
<Datagrid>
28+
<TextField source="id" />
29+
<TextField source="title" />
30+
<DateField source="author" />
31+
</Datagrid>
32+
</InfiniteList>
33+
);
34+
35+
// in src/App.js
36+
import { Admin, Resource } from 'react-admin';
37+
import jsonServerProvider from 'ra-data-json-server';
38+
39+
import { BookList } from './books';
40+
41+
const App = () => (
42+
<Admin dataProvider={jsonServerProvider('https://jsonplaceholder.typicode.com')}>
43+
<Resource name="books" list={BookList} />
44+
</Admin>
45+
);
46+
47+
export default App;
48+
```
49+
50+
That's enough to display a basic post list, that users can sort and filter, and load additional records when they reach the bottom of the list.
51+
52+
**Tip**: `<Datagrid>` has a sticky header by default, so the user can always see the column names when they scroll down.
53+
54+
## Props
55+
56+
The props are the same as [the `<List>` component](./List.md):
57+
58+
| Prop | Required | Type | Default | Description |
59+
|----------------------------|----------|----------------|-------------------------|----------------------------------------------------------------------------------------------|
60+
| `children` | Required | `ReactNode` | - | The component to use to render the list of records. |
61+
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. |
62+
| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. |
63+
| `component` | Optional | `Component` | `Card` | The component to render as the root element. |
64+
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. |
65+
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. |
66+
| `disable SyncWithLocation` | Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. |
67+
| `empty` | Optional | `ReactElement` | - | The component to display when the list is empty. |
68+
| `empty WhileLoading` | Optional | `boolean` | `false` | Set to `true` to return `null` while the list is loading. |
69+
| `exporter` | Optional | `function` | - | The function to call to export the list. |
70+
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
71+
| `filter` | Optional | `object` | - | The permanent filter values. |
72+
| `filter DefaultValues` | Optional | `object` | - | The default filter values. |
73+
| `hasCreate` | Optional | `boolean` | `false` | Set to `true` to show the create button. |
74+
| `pagination` | Optional | `ReactElement` | `<Infinite Pagination>` | The pagination component to use. |
75+
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
76+
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
77+
| `resource` | Optional | `string` | - | The resource name, e.g. `posts`. |
78+
| `sort` | Optional | `object` | - | The initial sort parameters. |
79+
| `storeKey` | Optional | `string` | - | The key to use to store the current filter & sort. |
80+
| `title` | Optional | `string` | - | The title to display in the App Bar. |
81+
| `sx` | Optional | `object` | - | The CSS styles to apply to the component. |
82+
83+
Check the [`<List>` component](./List.md) for details about each prop.
84+
85+
Additional props are passed down to the root component (a MUI `<Card>` by default).
86+
87+
## `pagination`
88+
89+
You can replace the default "load on scroll" pagination (triggered by a component named `<InfinitePagination>`) by a custom pagination component. To get the pagination state and callbacks, you'll need to read the `InfinitePaginationContext`.
90+
91+
![load more button](./img/infinite-pagination-load-more.webp)
92+
93+
For example, here is a custom infinite pagination component displaying a "Load More" button at the bottom of the list:
94+
95+
```jsx
96+
import { InfiniteList, useInfinitePaginationContext, Datagrid, TextField } from 'react-admin';
97+
import { Box, Button } from '@mui/material';
98+
99+
const LoadMore = () => {
100+
const {
101+
hasNextPage,
102+
fetchNextPage,
103+
isFetchingNextPage,
104+
} = useInfinitePaginationContext();
105+
return hasNextPage ? (
106+
<Box mt={1} textAlign="center">
107+
<Button
108+
disabled={isFetchingNextPage}
109+
onClick={() => fetchNextPage()}
110+
>
111+
Load more
112+
</Button>
113+
</Box>
114+
) : null;
115+
};
116+
117+
export const BookList = () => (
118+
<InfiniteList pagination={<LoadMore />}>
119+
<Datagrid>
120+
<TextField source="id" />
121+
<TextField source="title" />
122+
<TextField source="author" />
123+
</Datagrid>
124+
</InfiniteList>
125+
);
126+
```
127+
128+
## Showing The Record Count
129+
130+
One drawback of the `<InfiniteList>` component is that it doesn't show the number of results. To fix this, you can use `useListContext` to access the `total` property of the list, and render the total number of results in a sticky footer:
131+
132+
![Infinite list with total number of results](./img/infinite-pagination-count.webp)
133+
134+
{% raw %}
135+
```jsx
136+
import { useListContext, InfinitePagination, InfiniteList } from 'react-admin';
137+
import { Box, Card, Typography } from '@mui/material';
138+
139+
const CustomPagination = () => {
140+
const { total } = useListContext();
141+
return (
142+
<>
143+
<InfinitePagination />
144+
{total > 0 && (
145+
<Box position="sticky" bottom={0} textAlign="center">
146+
<Card
147+
elevation={2}
148+
sx={{ px: 2, py: 1, mb: 1, display: 'inline-block' }}
149+
>
150+
<Typography variant="body2">{total} results</Typography>
151+
</Card>
152+
</Box>
153+
)}
154+
</>
155+
);
156+
};
157+
158+
export const BookList = () => (
159+
<InfiniteList pagination={<CustomPagination />}>
160+
// ...
161+
</InfiniteList>
162+
);
163+
```
164+
{% endraw %}

docs/List.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,35 @@ That's enough to display a basic post list, with functional sort and pagination:
4848

4949
You can find more advanced examples of `<List>` usage in the [demos](./Demos.md).
5050

51+
## Props
52+
53+
| Prop | Required | Type | Default | Description |
54+
|---------------------------|----------|----------------|----------------|----------------------------------------------------------------------------------------------|
55+
| `children` | Required | `ReactNode` | - | The component to use to render the list of records. |
56+
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. |
57+
| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. |
58+
| `component` | Optional | `Component` | `Card` | The component to render as the root element. |
59+
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. |
60+
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. |
61+
| `disable SyncWithLocation`| Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. |
62+
| `empty` | Optional | `ReactElement` | - | The component to display when the list is empty. |
63+
| `emptyWhileLoading` | Optional | `boolean` | `false` | Set to `true` to return `null` while the list is loading. |
64+
| `exporter` | Optional | `function` | - | The function to call to export the list. |
65+
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
66+
| `filter` | Optional | `object` | - | The permanent filter values. |
67+
| `filterDefaultValues` | Optional | `object` | - | The default filter values. |
68+
| `hasCreate` | Optional | `boolean` | `false` | Set to `true` to show the create button. |
69+
| `pagination` | Optional | `ReactElement` | `<Pagination>` | The pagination component to use. |
70+
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
71+
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
72+
| `resource` | Optional | `string` | - | The resource name, e.g. `posts`. |
73+
| `sort` | Optional | `object` | - | The initial sort parameters. |
74+
| `storeKey` | Optional | `string` | - | The key to use to store the current filter & sort. |
75+
| `title` | Optional | `string` | - | The title to display in the App Bar. |
76+
| `sx` | Optional | `object` | - | The CSS styles to apply to the component. |
77+
78+
Additional props are passed down to the root component (a MUI `<Card>` by default).
79+
5180
## `actions`
5281

5382
![Actions Toolbar](./img/actions-toolbar.png)
@@ -865,6 +894,41 @@ const PostList = () => (
865894
```
866895
{% endraw %}
867896

897+
## Infinite Scroll Pagination
898+
899+
By default, the `<List>` component displays the first page of the list of records. To display the next page, the user must click on the "next" button. This is called "finite pagination". An alternative is to display the next page automatically when the user scrolls to the bottom of the list. This is called "infinite pagination".
900+
901+
<video controls autoplay muted loop width="100%">
902+
<source src="./img/infinite-book-list.webm" poster="./img/infinite-book-list.webp" type="video/webm">
903+
Your browser does not support the video tag.
904+
</video>
905+
906+
To achieve infinite pagination, replace the `<List>` component with [the `<InfiniteList>` component](./InfiniteList.md).
907+
908+
```diff
909+
import {
910+
- List,
911+
+ InfiniteList,
912+
Datagrid,
913+
TextField,
914+
DateField
915+
} from 'react-admin';
916+
917+
const BookList = () => (
918+
- <List>
919+
+ <InfiniteList>
920+
<Datagrid>
921+
<TextField source="id" />
922+
<TextField source="title" />
923+
<DateField source="author" />
924+
</Datagrid>
925+
- </List>
926+
+ </InfiniteList>
927+
);
928+
```
929+
930+
`<InfiniteList>` is a drop-in replacement for `<List>`. It accepts the same props, and uses the same view layout. Check [the `<InfiniteList>` documentation](./InfiniteList.md) for more information.
931+
868932
## Live Updates
869933

870934
If you want to subscribe to live updates on the list of records (topic: `resource/[resource]`), use [the `<ListLive>` component](./ListLive.md) instead.

docs/Pagination.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,40 @@ const PostPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100]}
4545
```
4646

4747
**Tip**: Pass an empty array to `rowsPerPageOptions` to disable the rows per page selection.
48+
49+
## Infinite Scroll
50+
51+
On mobile devices, the `<Pagination>` component is not very user friendly. The expected user experience is to reveal more records when the user scrolls to the bottom of the list. This UX is also useful on desktop, for lists with a large number of records.
52+
53+
<video controls autoplay muted loop width="100%">
54+
<source src="./img/infinite-book-list.webm" poster="./img/infinite-book-list.webp" type="video/webm">
55+
Your browser does not support the video tag.
56+
</video>
57+
58+
To achieve this, you can use the `<InfiniteList>` component instead of the `<List>` component.
59+
60+
```diff
61+
import {
62+
- List,
63+
+ InfiniteList,
64+
Datagrid,
65+
TextField,
66+
DateField
67+
} from 'react-admin';
68+
69+
const BookList = () => (
70+
- <List>
71+
+ <InfiniteList>
72+
<Datagrid>
73+
<TextField source="id" />
74+
<TextField source="title" />
75+
<DateField source="author" />
76+
</Datagrid>
77+
- </List>
78+
+ </InfiniteList>
79+
);
80+
```
81+
82+
`<InfiniteList>` uses a special pagination component, `<InfinitePagination>`, which doesn't display any pagination buttons. Instead, it displays a loading indicator when the user scrolls to the bottom of the list. But you cannot use this `<InfinitePagination>` inside a regular `<List>` component.
83+
84+
For more information, see [the `<InfiniteList>` documentation](./InfiniteList.md).

docs/Reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ title: "Index"
8585

8686
**- I -**
8787
* [`<IfCanAccess>`](./IfCanAccess.md)<img class="icon" src="./img/premium.svg" />
88+
* [`<InfiniteList>`](./InfiniteList.md)
89+
* [`<InfinitePagination>`](./InfiniteList.md)
8890
* [`<ImageField>`](./ImageField.md)
8991
* [`<ImageInput>`](./ImageInput.md)
9092
* [`<ImageInputPreview>`](./ImageInput.md#imageinput)

docs/img/infinite-book-list.webm

283 KB
Binary file not shown.

docs/img/infinite-book-list.webp

14.5 KB
Loading
23.8 KB
Loading
21.6 KB
Loading

docs/navigation.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<li {% if page.path == 'List.md' %} class="active" {% endif %}><a class="nav-link" href="./List.html"><code>&lt;List&gt;</code></a></li>
6868
<li {% if page.path == 'ListBase.md' %} class="active" {% endif %}><a class="nav-link" href="./ListBase.html"><code>&lt;ListBase&gt;</code></a></li>
6969
<li {% if page.path == 'ListGuesser.md' %} class="active" {% endif %}><a class="nav-link" href="./ListGuesser.html"><code>&lt;ListGuesser&gt;</code></a></li>
70+
<li {% if page.path == 'InfiniteList.md' %} class="active" {% endif %}><a class="nav-link" href="./InfiniteList.html"><code>&lt;InfiniteList&gt;</code></a></li>
7071
<li {% if page.path == 'TreeWithDetails.md' %} class="active" {% endif %}><a class="nav-link" href="./TreeWithDetails.html"><code>&lt;TreeWithDetails&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
7172
<li {% if page.path == 'Datagrid.md' %} class="active" {% endif %}><a class="nav-link" href="./Datagrid.html"><code>&lt;Datagrid&gt;</code></a></li>
7273
<li {% if page.path == 'SimpleList.md' %} class="active" {% endif %}><a class="nav-link" href="./SimpleList.html"><code>&lt;SimpleList&gt;</code></a></li>

0 commit comments

Comments
 (0)