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 ability to change the sort, filter and selection of ArrayField #8802

Merged
merged 4 commits into from
Apr 5, 2023
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
191 changes: 176 additions & 15 deletions docs/ArrayField.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ title: "The ArrayField Component"

# `<ArrayField>`

Display a collection using `<Field>` child components.
`<ArrayField>` renders an embedded array of objects.

Ideal for embedded arrays of objects, e.g. `tags` and `backlinks` in the following `post` object:
![ArrayField](./img/array-field.webp)

`<ArrayField>` creates a [`ListContext`](./useListContext.md) with the field value, and renders its children components - usually iterator components like [`<Datagrid>`](./Datagrid.md) or [`<SingleFieldList>`](./SingleFieldList.md).

## Usage

`<ArrayField>` is ideal for collections of objects, e.g. `tags` and `backlinks` in the following `post` object:

```js
{
id: 123,
tags: [
{ name: 'foo' },
{ name: 'bar' }
],
title: 'Lorem Ipsum Sit Amet',
tags: [{ name: 'dolor' }, { name: 'sit' }, { name: 'amet' }],
backlinks: [
{
uuid: '34fdf393-f449-4b04-a423-38ad02ae159e',
Expand All @@ -31,34 +35,191 @@ Ideal for embedded arrays of objects, e.g. `tags` and `backlinks` in the followi
}
```

The child must be an iterator component (like `<Datagrid>` or `<SingleFieldList>`).
Leverage `<ArrayField>` e.g. in a Show view, to display the `tags` as a `<SingleFieldList>` and the `backlinks` as a `<Datagrid>`:

```jsx
import {
ArrayField,
ChipField,
Datagrid,
Show,
SimpleShowLayout,
SingleFieldList,
TextField
} from 'react-admin';

const PostShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<ArrayField source="tags">
<SingleFieldList>
<ChipField source="name" size="small" />
</SingleFieldList>
</ArrayField>
<ArrayField source="backlinks">
<Datagrid bulkActionButtons={false}>
<TextField source="uuid" />
<TextField source="date" />
<TextField source="url" />
</Datagrid>
</ArrayField>
</SimpleShowLayout>
</Show>
)
```

## Props

| Prop | Required | Type | Default | Description |
|------------|----------|-------------------|---------|------------------------------------------|
| `children` | Required | `ReactNode` | | The component to render the list. |
| `filter` | Optional | `object` | | The filter to apply to the list. |
| `perPage` | Optional | `number` | 1000 | The number of items to display per page. |
| `sort` | Optional | `{ field, order}` | | The sort to apply to the list. |

`<ArrayField>` accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead).

`<ArrayField>` relies on [`useList`](./useList.md) to filter, paginate, and sort the data, so it accepts the same props.

## `children`

Here is how to display all the backlinks of the current post as a `<Datagrid>`:
`<ArrayField>` renders its `children` component wrapped in a [`<ListContextProvider>`](./useListContext.md). Commonly used child components are [`<Datagrid>`](./Datagrid.md), [`<SingleFieldList>`](./SingleFieldList.md), and [`<SimpleList>`](./SimpleList.md).

```jsx
{/* using SingleFieldList as child */}
<ArrayField source="tags">
<SingleFieldList>
<ChipField source="name" />
</SingleFieldList>
</ArrayField>

{/* using Datagrid as child */}
<ArrayField source="backlinks">
<Datagrid>
<DateField source="date" />
<UrlField source="url" />
<TextField source="uuid" />
<TextField source="date" />
<TextField source="url" />
</Datagrid>
</ArrayField>

{/* using SimpleList as child */}
<ArrayField source="backlinks">
<SimpleList
primaryText={record => record.url}
secondaryText={record => record.date}
/>
</ArrayField>
```

And here is how to display all the tags of the current post as `<Chip>` components:
## `filter`

You can use the `filter` prop to display only a subset of the items in the array. For instance, to display only the backlinks for a particular day:

{% raw %}
```jsx
<ArrayField source="tags">
<ArrayField source="backlinks" filter={{ date: '2012-08-10T00:00:00.000Z' }}>
<Datagrid>
<TextField source="uuid" />
<TextField source="date" />
<TextField source="url" />
</Datagrid>
</ArrayField>
```
{% endraw %}

The filtering capabilities are very limited. For instance, there is no "greater than" or "less than" operator. You can only filter on the equality of a field.

## `perPage`

If the value is a large array, and you don't need to display all the items, you can use the `perPage` prop to limit the number of items displayed.

As `<ArrayField>` creates a [`ListContext`](./useListContext.md), you can use the `<Pagination>` component to navigate through the items.

```jsx
import {
ArrayField,
Datagrid,
Pagination,
Show,
SimpleShowLayout,
TextField
} from 'react-admin';

const PostShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<ArrayField source="backlinks" perPage={5}>
<Datagrid>
<TextField source="uuid" />
<TextField source="date" />
<TextField source="url" />
</Datagrid>
<Pagination />
</ArrayField>
</SimpleShowLayout>
</Show>
);
```

## `sort`

By default, `<ArrayField>` displays the items in the order they are stored in the field. You can use the `sort` prop to change the sort order.

{% raw %}
```jsx
<ArrayField source="tags" sort={{ field: 'name', order: 'ASC' }}>
<SingleFieldList>
<ChipField source="name" />
</SingleFieldList>
</ArrayField>
```
{% endraw %}

## Properties
## Using The List Context

`<ArrayField>` accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead).
`<ArrayField>` creates a [`ListContext`](./useListContext.md) with the field value, so you can use any of the list context values in its children. This includes callbacks to sort, filter, and select items.

For instance, you can make the chips selectable as follows:

```jsx
const SelectedChip = () => {
const { selectedIds, onToggleItem } = useListContext();
const record = useRecordContext();
return (
<ChipField
source="title"
clickable
onClick={() => {
onToggleItem(record.id);
}}
color={selectedIds.includes(record.id) ? 'primary' : 'default'}
/>
);
};

const PostShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<ArrayField source="tags">
<SingleFieldList>
<SelectedChip />
</SingleFieldList>
</ArrayField>
</SimpleShowLayout>
</Show>
)
```

**Tip**: The selection logic uses the `id` field for each collection element, so the above example assumes that the `tags` field contains objects like `{ id: 123, name: 'bar' }`.

Check [the `useListContext` documentation](./useListContext.md) for more information on the list context values.

## Rendering An Array Of Strings

**Tip**: If you need to render a custom collection, it's often simpler to write your own component:
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), it's often simpler to write your own component:

```jsx
import { useRecordContext } from 'react-admin';
Expand Down
Binary file added docs/img/array-field.webp
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/useList.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ const { data, total } = useList({
// data will be [{ id: 1, name: 'Arnold' }] and total will be 1
```

The filtering capabilities are very limited. For instance, there is no "greater than" or "less than" operator. You can only filter on the equality of a field.

## `filterCallback`

Property for custom filter definition. Lets you apply local filters to the fetched data.
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/src/posts/PostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const PostList = () => {
cellClassName="hiddenOnSmallScreens"
headerClassName="hiddenOnSmallScreens"
>
<SingleFieldList>
<SingleFieldList sx={{ my: -2 }}>
<ChipField source="name.en" size="small" />
</SingleFieldList>
</ReferenceArrayField>
Expand Down
5 changes: 4 additions & 1 deletion examples/simple/src/posts/PostShow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ const PostShow = () => {
sort={{ field: `name.${locale}`, order: 'ASC' }}
>
<SingleFieldList>
<ChipField source={`name.${locale}`} />
<ChipField
source={`name.${locale}`}
size="small"
/>
</SingleFieldList>
</ReferenceArrayField>
<DateField source="published_at" />
Expand Down
26 changes: 25 additions & 1 deletion packages/ra-ui-materialui/src/field/ArrayField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import {
CoreAdminContext,
ResourceContextProvider,
Expand All @@ -12,6 +12,7 @@ import { NumberField } from './NumberField';
import { TextField } from './TextField';
import { Datagrid } from '../list';
import { SimpleList } from '../list';
import { ListContext } from './ArrayField.stories';

describe('<ArrayField />', () => {
const sort = { field: 'id', order: 'ASC' };
Expand Down Expand Up @@ -130,4 +131,27 @@ describe('<ArrayField />', () => {
expect(queryByText('baz')).not.toBeNull();
expect(queryByText('456')).not.toBeNull();
});

it('should create a ListContext with working callbacks', async () => {
render(<ListContext />);
screen.getByText('War and Peace');
screen.getByText('Filter by title').click();
await waitFor(() => {
expect(screen.queryByText('War and Peace')).toBeNull();
});
const chip = screen.getByText('Resurrection');
expect(
(chip.parentNode as HTMLElement).className.includes(
'MuiChip-colorDefault'
)
).toBeTruthy();
chip.click();
await waitFor(() => {
expect(
(chip.parentNode as HTMLElement).className.includes(
'MuiChip-colorPrimary'
)
).toBeTruthy();
});
});
});
Loading