List Layout
Arrange the list header, actions, and table, or replace the DataTable entirely.
<List> renders a thin shell: a title, a header actions slot, and its
children. It has no default child. Whatever you put inside is all the user
sees, so a list needs a <DataTable> (or your own component) and usually a
<ListPagination>.
The header
<List> shows a title (the resource label, pluralized) and an actions slot on
the right. Pass actions to fill it, typically the create / export / filter
buttons, or actions={false} to omit it. Pass title to override the heading,
or title={false} to drop it:
import {
CreateButton,
DataTable,
ExportButton,
List,
ListPagination,
} from "@chelone/react";
export function PostList() {
return (
<List
title="All posts"
actions={
<>
<CreateButton />
<ExportButton />
</>
}
>
<DataTable>
<DataTable.Col source="title" />
</DataTable>
<ListPagination />
</List>
);
}The table body
Inside <List>, a <DataTable> renders one <DataTable.Col> per column. A
column either points at a source (and renders that value), wraps a field
component, or supplies a render function. render receives the row record
with every value typed unknown, so narrow a value before you use it:
import {
DataTable,
ReferenceField,
TextField,
} from "@chelone/react";
export function PostTable() {
return (
<DataTable>
{/* value straight from the record */}
<DataTable.Col source="title" />
{/* right-aligned numeric column */}
<DataTable.NumberCol source="views" />
{/* a field component reading the row record */}
<DataTable.Col label="Author">
<ReferenceField source="author_id" reference="authors">
<TextField source="name" />
</ReferenceField>
</DataTable.Col>
{/* an inline render for a computed cell */}
<DataTable.Col
label="Excerpt"
render={(record) => String(record.body ?? "").slice(0, 40)}
/>
</DataTable>
);
}Bulk actions
A <DataTable> shows no checkboxes and no bulk toolbar by default.
bulkActionButtons defaults to false. Pass the buttons you want and the table
adds a selection column; the toolbar appears once at least one row is selected:
import {
BulkDeleteButton,
BulkExportButton,
DataTable,
List,
ListPagination,
} from "@chelone/react";
export function PostList() {
return (
<List>
<DataTable
bulkActionButtons={
<>
<BulkExportButton />
<BulkDeleteButton />
</>
}
>
<DataTable.Col source="title" />
<DataTable.NumberCol source="views" />
</DataTable>
<ListPagination />
</List>
);
}For a toolbar of your own, pass bulkActionsToolbar instead. It replaces
bulkActionButtons, and a <BulkActionsToolbar> is the stock shell to put your
buttons in.
Let users choose columns
Put a <ColumnsButton> in the list actions
and the user can show, hide and reorder the columns of the <DataTable> below
it:
import {
ColumnsButton,
CreateButton,
DataTable,
List,
ListPagination,
} from "@chelone/react";
export function PostList() {
return (
<List
actions={
<>
<ColumnsButton />
<CreateButton />
</>
}
>
<DataTable defaultHiddenColumns={["body"]}>
<DataTable.Col source="title" />
<DataTable.Col source="body" />
<DataTable.NumberCol source="views" />
</DataTable>
<ListPagination />
</List>
);
}The button needs a store on <Admin> and throws without one. The table
publishes its columns into that store and the button reads them back, so the
button stays disabled until a <DataTable> has mounted.
- The hidden list is kept under
<resource>.datatableand the order under<resource>.datatable_columnOrder. AlocalStorageAdapterstore keeps both across reloads. defaultHiddenColumnson<DataTable>lists the column ids hidden until the user stores a choice. A column's id is itssource, or itslabelwhen it has a string one.storeKeyon<DataTable>replaces the<resource>.datatableprefix, for a second table on one resource. Give<ColumnsButton>the samestoreKey.- A column with no
sourceand no stringlabelhas no id. It is always shown and the panel does not list it.
A list that is a tree rather than a table can use
<TreeList>, which nests the records by a parent
field.
Replace the table
<List> only requires that its child reads the list context. To build a custom
layout (cards, a gallery, a grid) read the records with useListContext
directly and render them however you like. The context is not generic, so its
records are typed { id } only. Cast the result to the shape of your resource:
import {
List,
ListPagination,
type UseListControllerResult,
useListContext,
} from "@chelone/react";
type Post = { id: number; title: string };
function PostCards() {
const { data, isPending } = useListContext() as UseListControllerResult<Post>;
if (isPending) return <p>Loading…</p>;
return (
<div className="grid grid-cols-3 gap-4">
{data?.map((post) => (
<article key={post.id} className="rounded border p-4">
<h3>{post.title}</h3>
</article>
))}
</div>
);
}
export function PostList() {
return (
<List>
<PostCards />
<ListPagination />
</List>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| actions | false | ReactElement<unknown, string | JSXElementConstructor<any>> | — | Header actions (create/export/filter buttons); nothing by default. |
| children | ReactNode | — | |
| className | string | — | |
| component | ElementType<any, keyof IntrinsicElements> | — | Element/component wrapping the children (defaults to `"div"`). |
| perPage | number | — | Rows-per-page for the initial query. |
| resource | string | — | Override the ambient resource (defaults to the `<Resource>` context). |
| title | ReactNode | — | Header title. A string/element renders as an `<h2>`; `false` omits it. |
Tips
- The
componentprop swaps the element wrapping the children (default"div"). Pass a layout component that acceptsclassNameandchildrento frame the body. - Selection lives in the list context too (
selectedIds,toggleItem,unselectAll). See Advanced for a bulk toolbar in a custom layout.