Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add <InfiniteList> and <InfinitePagination> components #8781

Merged
merged 29 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9bc9a2e
Add infinite list core hooks and components
fzaninotto Mar 29, 2023
15382dc
Add story to test base case
fzaninotto Mar 29, 2023
7e6d5a6
Add InfiniteList component
fzaninotto Mar 29, 2023
df9567a
Remove need for i18n
fzaninotto Mar 29, 2023
c82e0dc
Test more props in storybook
fzaninotto Mar 29, 2023
e5952d9
Rename basic story
fzaninotto Mar 29, 2023
f821f03
Rename InfiniteScroll to InfinitePagination
fzaninotto Mar 29, 2023
5a55eee
Do not set sort by default on the controller
fzaninotto Mar 29, 2023
8635bb4
Use InfiniteList in simple example
fzaninotto Mar 29, 2023
86d676c
Fix useUpdate optimistic effect with infinite list
fzaninotto Mar 31, 2023
de8e225
Update useGetMany to update getInfiniteList cache
fzaninotto Mar 31, 2023
f344252
Add tests for useUpdate and useUpdateMany
fzaninotto Mar 31, 2023
71e57f8
Update useDelete optimistic for getInfiniteList
fzaninotto Mar 31, 2023
b138935
update useDeleteMany cache
fzaninotto Mar 31, 2023
4aefedd
Add documentation
fzaninotto Apr 2, 2023
dc754b4
Document how to show the count
fzaninotto Apr 2, 2023
92dbfa4
Add mention of sticky header and footer in the docs
fzaninotto Apr 2, 2023
0d0baa0
Fix sidebar starts open on Mobile
fzaninotto Apr 3, 2023
6a9cb27
Add e2e test for infinite pagination
fzaninotto Apr 3, 2023
ba6a82e
Add unit test for InfiniteListBase
fzaninotto Apr 3, 2023
0972010
Fix total when getList returns a pageInfo
fzaninotto Apr 3, 2023
c84f432
Update packages/ra-core/src/controller/list/useInfiniteListController.ts
fzaninotto Apr 4, 2023
81ad790
Update packages/ra-core/src/controller/list/useInfiniteListController.ts
fzaninotto Apr 4, 2023
4f5c884
Fix InfiniteList example
fzaninotto Apr 4, 2023
687bbff
Add entries in Reference
fzaninotto Apr 4, 2023
aa6266c
Add load more story and documentation
fzaninotto Apr 4, 2023
dacbbe6
Fix bug where the InfinitePagination cannot fetch after page 2
fzaninotto Apr 4, 2023
5345571
Fix InfiniteList stories share the same list state
fzaninotto Apr 5, 2023
1a25332
Merge branch 'next' into infinite-list
fzaninotto Apr 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cypress/e2e/mobile.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import listPageFactory from '../support/ListPage';

describe('Mobile UI', () => {
const ListPagePosts = listPageFactory('/#/posts');

beforeEach(() => {
window.localStorage.clear();
cy.viewport('iphone-x');
});

describe('Infinite Scroll', () => {
it.only('should load more items when scrolling to the bottom of the page', () => {
ListPagePosts.navigate();
cy.contains('Sed quo et et fugiat modi').should('not.exist');
cy.scrollTo('bottom');
cy.wait(500);
cy.scrollTo('bottom');
cy.contains('Sed quo et et fugiat modi');
});
});
});
164 changes: 164 additions & 0 deletions docs/InfiniteList.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
layout: default
title: "The InfiniteList Component"
---

# `<InfiniteList>`

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.

<video controls autoplay muted loop width="100%">
<source src="./img/infinite-book-list.webm" poster="./img/infinite-book-list.webp" type="video/webm">
Your browser does not support the video tag.
</video>

`<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.

## Usage

Here is the minimal code necessary to display a list of books with infinite scroll:

```jsx
// in src/books.js
import { InfiniteList, Datagrid, TextField, DateField } from 'react-admin';

export const BookList = () => (
<InfiniteList>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<DateField source="author" />
</Datagrid>
</InfiniteList>
);

// in src/App.js
import { Admin, Resource } from 'react-admin';
import jsonServerProvider from 'ra-data-json-server';

import { BookList } from './books';

const App = () => (
<Admin dataProvider={jsonServerProvider('https://jsonplaceholder.typicode.com')}>
<Resource name="books" list={BookList} />
</Admin>
);

export default App;
```

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.

**Tip**: `<Datagrid>` has a sticky header by default, so the user can always see the column names when they scroll down.

## Props

The props are the same as [the `<List>` component](./List.md):

| Prop | Required | Type | Default | Description |
|----------------------------|----------|----------------|-------------------------|----------------------------------------------------------------------------------------------|
| `children` | Required | `ReactNode` | - | The component to use to render the list of records. |
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. |
| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. |
| `component` | Optional | `Component` | `Card` | The component to render as the root element. |
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. |
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. |
| `disable SyncWithLocation` | Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. |
| `empty` | Optional | `ReactElement` | - | The component to display when the list is empty. |
| `empty WhileLoading` | Optional | `boolean` | `false` | Set to `true` to return `null` while the list is loading. |
| `exporter` | Optional | `function` | - | The function to call to export the list. |
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
| `filter` | Optional | `object` | - | The permanent filter values. |
| `filter DefaultValues` | Optional | `object` | - | The default filter values. |
| `hasCreate` | Optional | `boolean` | `false` | Set to `true` to show the create button. |
| `pagination` | Optional | `ReactElement` | `<Infinite Pagination>` | The pagination component to use. |
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
| `resource` | Optional | `string` | - | The resource name, e.g. `posts`. |
| `sort` | Optional | `object` | - | The initial sort parameters. |
| `storeKey` | Optional | `string` | - | The key to use to store the current filter & sort. |
| `title` | Optional | `string` | - | The title to display in the App Bar. |
| `sx` | Optional | `object` | - | The CSS styles to apply to the component. |

Check the [`<List>` component](./List.md) for details about each prop.

Additional props are passed down to the root component (a MUI `<Card>` by default).

## `pagination`

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`.

![load more button](./img/infinite-pagination-load-more.webp)

For example, here is a custom infinite pagination component displaying a "Load More" button at the bottom of the list:

```jsx
import { InfiniteList, useInfinitePaginationContext, Datagrid, TextField } from 'react-admin';
import { Box, Button } from '@mui/material';

const LoadMore = () => {
const {
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useInfinitePaginationContext();
return hasNextPage ? (
<Box mt={1} textAlign="center">
<Button
disabled={isFetchingNextPage}
onClick={() => fetchNextPage()}
>
Load more
</Button>
</Box>
) : null;
};

export const BookList = () => (
<InfiniteList pagination={<LoadMore />}>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="author" />
</Datagrid>
</InfiniteList>
);
```

## Showing The Record Count

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:

![Infinite list with total number of results](./img/infinite-pagination-count.webp)

{% raw %}
```jsx
import { useListContext, InfinitePagination, InfiniteList } from 'react-admin';
import { Box, Card, Typography } from '@mui/material';

const CustomPagination = () => {
const { total } = useListContext();
return (
<>
<InfinitePagination />
{total > 0 && (
<Box position="sticky" bottom={0} textAlign="center">
<Card
elevation={2}
sx={{ px: 2, py: 1, mb: 1, display: 'inline-block' }}
>
<Typography variant="body2">{total} results</Typography>
</Card>
</Box>
)}
</>
);
};
Comment on lines +139 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make a component out of this too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I don't think this is a general solution, it's just an example. People will want to put the number of results on top of the list, in a datagrid footer, in the action bar, etc... There is no standard for that UI.


export const BookList = () => (
<InfiniteList pagination={<CustomPagination />}>
// ...
</InfiniteList>
);
```
{% endraw %}
64 changes: 64 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ That's enough to display a basic post list, with functional sort and pagination:

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

## Props

| Prop | Required | Type | Default | Description |
|---------------------------|----------|----------------|----------------|----------------------------------------------------------------------------------------------|
| `children` | Required | `ReactNode` | - | The component to use to render the list of records. |
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. |
| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. |
| `component` | Optional | `Component` | `Card` | The component to render as the root element. |
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. |
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. |
| `disable SyncWithLocation`| Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. |
| `empty` | Optional | `ReactElement` | - | The component to display when the list is empty. |
| `emptyWhileLoading` | Optional | `boolean` | `false` | Set to `true` to return `null` while the list is loading. |
| `exporter` | Optional | `function` | - | The function to call to export the list. |
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
| `filter` | Optional | `object` | - | The permanent filter values. |
| `filterDefaultValues` | Optional | `object` | - | The default filter values. |
| `hasCreate` | Optional | `boolean` | `false` | Set to `true` to show the create button. |
| `pagination` | Optional | `ReactElement` | `<Pagination>` | The pagination component to use. |
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
| `resource` | Optional | `string` | - | The resource name, e.g. `posts`. |
| `sort` | Optional | `object` | - | The initial sort parameters. |
| `storeKey` | Optional | `string` | - | The key to use to store the current filter & sort. |
| `title` | Optional | `string` | - | The title to display in the App Bar. |
| `sx` | Optional | `object` | - | The CSS styles to apply to the component. |

Additional props are passed down to the root component (a MUI `<Card>` by default).

## `actions`

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

## Infinite Scroll Pagination

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".

<video controls autoplay muted loop width="100%">
<source src="./img/infinite-book-list.webm" poster="./img/infinite-book-list.webp" type="video/webm">
Your browser does not support the video tag.
</video>

To achieve infinite pagination, replace the `<List>` component with [the `<InfiniteList>` component](./InfiniteList.md).

```diff
import {
- List,
+ InfiniteList,
Datagrid,
TextField,
DateField
} from 'react-admin';

const BookList = () => (
- <List>
+ <InfiniteList>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<DateField source="author" />
</Datagrid>
- </List>
+ </InfiniteList>
);
```

`<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.

## Live Updates

If you want to subscribe to live updates on the list of records (topic: `resource/[resource]`), use [the `<ListLive>` component](./ListLive.md) instead.
Expand Down
37 changes: 37 additions & 0 deletions docs/Pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,40 @@ const PostPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100]}
```

**Tip**: Pass an empty array to `rowsPerPageOptions` to disable the rows per page selection.

## Infinite Scroll

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.

<video controls autoplay muted loop width="100%">
<source src="./img/infinite-book-list.webm" poster="./img/infinite-book-list.webp" type="video/webm">
Your browser does not support the video tag.
</video>

To achieve this, you can use the `<InfiniteList>` component instead of the `<List>` component.

```diff
import {
- List,
+ InfiniteList,
Datagrid,
TextField,
DateField
} from 'react-admin';

const BookList = () => (
- <List>
+ <InfiniteList>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<DateField source="author" />
</Datagrid>
- </List>
+ </InfiniteList>
);
```

`<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.

For more information, see [the `<InfiniteList>` documentation](./InfiniteList.md).
2 changes: 2 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ title: "Index"

**- I -**
* [`<IfCanAccess>`](./IfCanAccess.md)<img class="icon" src="./img/premium.svg" />
* [`<InfiniteList>`](./InfiniteList.md)
* [`<InfinitePagination>`](./InfiniteList.md)
* [`<ImageField>`](./ImageField.md)
* [`<ImageInput>`](./ImageInput.md)
* [`<ImageInputPreview>`](./ImageInput.md#imageinput)
Expand Down
Binary file added docs/img/infinite-book-list.webm
Binary file not shown.
Binary file added docs/img/infinite-book-list.webp
Binary file not shown.
Binary file added docs/img/infinite-pagination-count.webp
Binary file not shown.
Binary file added docs/img/infinite-pagination-load-more.webp
Binary file not shown.
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<li {% if page.path == 'List.md' %} class="active" {% endif %}><a class="nav-link" href="./List.html"><code>&lt;List&gt;</code></a></li>
<li {% if page.path == 'ListBase.md' %} class="active" {% endif %}><a class="nav-link" href="./ListBase.html"><code>&lt;ListBase&gt;</code></a></li>
<li {% if page.path == 'ListGuesser.md' %} class="active" {% endif %}><a class="nav-link" href="./ListGuesser.html"><code>&lt;ListGuesser&gt;</code></a></li>
<li {% if page.path == 'InfiniteList.md' %} class="active" {% endif %}><a class="nav-link" href="./InfiniteList.html"><code>&lt;InfiniteList&gt;</code></a></li>
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
<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>
<li {% if page.path == 'Datagrid.md' %} class="active" {% endif %}><a class="nav-link" href="./Datagrid.html"><code>&lt;Datagrid&gt;</code></a></li>
<li {% if page.path == 'SimpleList.md' %} class="active" {% endif %}><a class="nav-link" href="./SimpleList.html"><code>&lt;SimpleList&gt;</code></a></li>
Expand Down
Loading