Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 2 additions & 4 deletions .lintstagedrc
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
{
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"git add",
"eslint --fix"
],
"*.{json,css,md}": [
"prettier",
"git add"
"prettier"
]
}
229 changes: 229 additions & 0 deletions docs/ListIterator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
---
layout: default
title: "ListIterator"
storybook_path: ra-core-controller-list-listiterator--using-render
---

# `<ListIterator>`

## Usage

Use the `<ListIterator>` component as a child of any component that provides a [`ListContext`](./useListContext.md):

- `<List>`,
- `<ListGuesser>`,
- `<ListBase>`,
- `<ReferenceArrayField>`,
- `<ReferenceManyField>`

{% raw %}
```jsx
import { ListBase, ListIterator } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';

const DashboardMostVisitedPosts = () => (
<ListBase resource="posts" sort={{ field: 'views', order: 'DESC' }} page={1} perPage={20}>
<OrderedList>
<ListIterator
render={record => <ListItem>{record.title} - {record.views}</ListItem>}
/>
</OrderedList>
</ListBase>
);
```
{% endraw %}

## Props

Here are all the props you can set on the `<AccordionForm>` component:

| Prop | Required | Type | Default | Description |
| ----------- | -------- | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------- |
| `children` | Optional | `ReactNode` | - | The content to render for each record |
| `data` | Optional | `RaRecord[]` | - | The records. Defaults to the `data` from the `ListContext` |
| `empty` | Optional | `ReactNode` | `null` | The content to display when there is no data |
| `isPending` | Optional | `boolean` | - | A boolean indicating whether the data is pending. Defaults to the `isPending` from the `ListContext` |
| `loading` | Optional | `ReactNode` | `null` | The content to display while the data is loading |
| `render` | Optional | `(record: RaRecord) => ReactNode` | - | A function that returns the content to render for each record |
| `total` | Optional | `number` | - | The total number of records. Defaults to the `total` from the `ListContext` |

Additional props are passed to `react-hook-form`'s [`useForm` hook](https://react-hook-form.com/docs/useform).

## `children`

If provided, `ListIterator` will render the `children` prop for each record. This is useful when the components you render leverages the [`RecordContext`](./useRecordContext.md):

{% raw %}
```tsx
import { ListBase, ListIterator, useRecordContext } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';

const DashboardMostVisitedPosts = () => (
<ListBase resource="posts" sort={{ field: 'views', order: 'DESC' }} page={1} perPage={20}>
<OrderedList>
<ListIterator>
<PostItem />
</ListIterator>
</OrderedList>
</ListBase>
);

const PostItem = () => {
const record = useRecordContext();
if (!record) return null;

return (
<ListItem>{record.title} - {record.views}</ListItem>
);
};
```
{% endraw %}

**Note**: You can't provide both the `children` and the `render` props. If both are provided, `<ListIterator>` will use the `render` prop.

## `data`

Although `<ListIterator>` reads the data from the closest [`<ListContext>`](./useListContext), you may provide it yourself when no such context is available:

{% raw %}
```jsx
import { ListIterator } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';
import { customerSegments } from './customerSegments.json';

const MyComponent = () => {
return (
<OrderedList>
<ListIterator
data={customerSegments}
total={customerSegments.length}
render={record => <ListItem>{record.name}</ListItem>}
/>
</OrderedList>
);
}
```
{% endraw %}

## `empty`

To provide a custom UI when there is no data, use the `empty` prop.

{% raw %}
```jsx
import { ListBase, ListIterator } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';

const DashboardMostVisitedPosts = () => (
<ListBase resource="posts" sort={{ field: 'views', order: 'DESC' }} page={1} perPage={20}>
<OrderedList>
<ListIterator
empty={<ListItem>No posts found</ListItem>}
render={record => <ListItem>{record.title} - {record.views}</ListItem>}
/>
</OrderedList>
</ListBase>
);
```
{% endraw %}

## `isPending`

Although `<ListIterator>` reads the `isPending` from the closest [`<ListContext>`](./useListContext), you may provide it yourself when no such context is available. This is useful when dealing with data not coming from the `dataProvider`:

{% raw %}
```tsx
import { ListBase, ListIterator } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';
import { useQuery } from '@tanstack/react-query';
import { fetchPostAnalytics } from './fetchPostAnalytics';

const DashboardMostVisitedPosts = () => {
const { data, isPending } = useQuery({
queryKey: ['dashboard', 'posts'],
queryFn: fetchPostAnalytics
});

return (
<OrderedList>
<ListIterator
data={data}
isPending={isPending}
render={record => <ListItem>{record.title} - {record.views}</ListItem>}
/>
</OrderedList>
);
}
```
{% endraw %}


## `loading`

To provide a custom UI while the data is loading use the `loading` prop.

{% raw %}
```jsx
import { ListBase, ListIterator } from 'react-admin';
import { OrderedList, ListItem, Skeleton } from 'my-favorite-ui-lib';

const DashboardMostVisitedPosts = () => (
<ListBase resource="posts" sort={{ field: 'views', order: 'DESC' }} page={1} perPage={20}>
<OrderedList>
<ListIterator
loading={<Skeleton />}
render={record => <ListItem>{record.title} - {record.views}</ListItem>}
/>
</OrderedList>
</ListBase>
);
```
{% endraw %}

## `render`

If provided, `ListIterator` will call the `render` prop for each record. This is useful when the components you render don't leverage the [`RecordContext`](./useRecordContext.md):

{% raw %}
```tsx
import { ListBase, ListIterator } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';

const DashboardMostVisitedPosts = () => (
<ListBase resource="posts" sort={{ field: 'views', order: 'DESC' }} page={1} perPage={20}>
<OrderedList>
<ListIterator
render={record => <ListItem>{record.title} - {record.views}</ListItem>}
/>
</OrderedList>
</ListBase>
);
```
{% endraw %}

**Note**: You can't provide both the `children` and the `render` props. If both are provided, `<ListIterator>` will use the `render` prop.

## `total`

Although `<ListIterator>` reads the total from the closest [`<ListContext>`](./useListContext), you may provide it yourself when no such context is available:

{% raw %}
```jsx
import { ListIterator } from 'react-admin';
import { OrderedList, ListItem } from 'my-favorite-ui-lib';
import { customerSegments } from './customerSegments.json';

const MyComponent = () => {
return (
<OrderedList>
<ListIterator
data={customerSegments}
total={customerSegments.length}
render={record => <ListItem>{record.name}</ListItem>}
/>
</OrderedList>
);
}
```
{% endraw %}

27 changes: 27 additions & 0 deletions packages/ra-core/src/controller/list/ListIterator.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { UsingChildren, UsingRender } from './ListIterator.stories';

describe('<ListIterator>', () => {
describe.each([
{ Story: UsingRender, prop: 'render' },
{ Story: UsingChildren, prop: 'children' },
])('Using the $prop prop', ({ Story }) => {
it('should render the records', async () => {
render(<Story />);

await screen.findByText('War and Peace');
await screen.findByText('The Lion, the Witch and the Wardrobe');
});
it('should render the pending prop when ListContext.isPending is true', async () => {
render(<Story isPending />);

await screen.findByText('Loading...');
});
it('should render the empty prop when there is no data', async () => {
render(<Story empty />);

await screen.findByText('No data');
});
});
});
120 changes: 120 additions & 0 deletions packages/ra-core/src/controller/list/ListIterator.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react';
import { useList, UseListOptions } from './useList';
import { ListContextProvider } from './ListContextProvider';
import { ListIterator } from './ListIterator';
import { useRecordContext } from '../record';

export default {
title: 'ra-core/controller/list/ListIterator',
};

const data = [
{ id: 1, title: 'War and Peace' },
{ id: 2, title: 'The Little Prince' },
{ id: 3, title: "Swann's Way" },
{ id: 4, title: 'A Tale of Two Cities' },
{ id: 5, title: 'The Lord of the Rings' },
{ id: 6, title: 'And Then There Were None' },
{ id: 7, title: 'Dream of the Red Chamber' },
{ id: 8, title: 'The Hobbit' },
{ id: 9, title: 'She: A History of Adventure' },
{ id: 10, title: 'The Lion, the Witch and the Wardrobe' },
];

export const UsingRender = ({
empty,
...props
}: UseListOptions & { empty?: boolean }) => {
const value = useList({
data: empty ? [] : data,
sort: { field: 'id', order: 'ASC' },
...props,
});

return (
<ListContextProvider value={value}>
<ul
style={{
listStyleType: 'none',
}}
>
<ListIterator
loading={<div>Loading...</div>}
empty={<div>No data</div>}
render={record => (
<li
style={{
padding: '10px',
border: '1px solid #ccc',
}}
>
{record.title}
</li>
)}
/>
</ul>
</ListContextProvider>
);
};

UsingRender.args = {
isPending: false,
empty: false,
};

UsingRender.argTypes = {
isPending: { control: 'boolean' },
empty: { control: 'boolean' },
};

const ListItem = () => {
const record = useRecordContext();
return (
<li
style={{
padding: '10px',
border: '1px solid #ccc',
}}
>
{record?.title}
</li>
);
};

export const UsingChildren = ({
empty,
...props
}: UseListOptions & { empty?: boolean }) => {
const value = useList({
data: empty ? [] : data,
sort: { field: 'id', order: 'ASC' },
...props,
});

return (
<ListContextProvider value={value}>
<ul
style={{
listStyleType: 'none',
}}
>
<ListIterator
loading={<div>Loading...</div>}
empty={<div>No data</div>}
>
<ListItem />
</ListIterator>
</ul>
</ListContextProvider>
);
};

UsingChildren.args = {
isPending: false,
empty: false,
};

UsingChildren.argTypes = {
isPending: { control: 'boolean' },
empty: { control: 'boolean' },
};
Loading
Loading