Table
Rows and columns of data with sorting, selection, nested rows and drag and drop. Describe the columns up front, then render a TableRow for every item.
import {
Badge,
Table,
TableCell,
TableRow,
TableThumbnail,
TableTitle
} from 'alinea/components'
const products = [
{id: 'oak-dining-chair', title: 'Oak dining chair', status: 'published'},
{id: 'walnut-stools', title: 'Walnut stools', status: 'draft'},
{id: 'linen-lounge-chair', title: 'Linen lounge chair', status: 'archived'}
] as const
export function TableExample() {
return (
<div style={{width: 520, height: 180}}>
<Table
aria-label="Products"
items={products}
columns={[
{id: 'image', header: 'Image', width: 72},
{id: 'title', header: 'Title', width: '1fr'},
{id: 'status', header: 'Status', width: 120}
]}
>
{product => (
<TableRow id={product.id} textValue={product.title}>
<TableThumbnail src={`/catalog/${product.id}.jpg`} alt="" />
<TableTitle title={product.title} label="Products" />
<TableCell>
<Badge size="sm" status={product.status}>
{product.status}
</Badge>
</TableCell>
</TableRow>
)}
</Table>
</div>
)
}Selection and sorting
Set a selectionMode to let editors select rows, showSelectionControls adds a checkbox to every row. Mark columns as sortable and sort the items in onSortChange.
import {
type SortDescriptor,
Table,
TableCell,
TableRow,
TableTitle
} from 'alinea/components'
import {useState} from 'react'
const products = [
{id: 'shirt', title: 'Linen shirt', price: 89},
{id: 'chair', title: 'Oak dining chair', price: 245},
{id: 'stool', title: 'Walnut stool', price: 160},
{id: 'lamp', title: 'Ceramic table lamp', price: 120}
]
export function TableSelectionExample() {
const [sort, setSort] = useState<SortDescriptor>({
column: 'title',
direction: 'asc'
})
const sorted = [...products].sort((a, b) =>
sort.column === 'price' ? a.price - b.price : a.title.localeCompare(b.title)
)
if (sort.direction === 'desc') sorted.reverse()
return (
<div style={{width: 520, height: 230}}>
<Table
aria-label="Products"
items={sorted}
columns={[
{id: 'title', header: 'Title', width: '1fr', sortable: true},
{
id: 'price',
header: 'Price',
width: 120,
align: 'end',
sortable: true
}
]}
selectionMode="multiple"
showSelectionControls
defaultSelectedKeys={new Set(['chair'])}
sortDescriptor={sort}
onSortChange={setSort}
>
{product => (
<TableRow id={product.id} textValue={product.title}>
<TableTitle title={product.title} />
<TableCell align="end">€{product.price}</TableCell>
</TableRow>
)}
</Table>
</div>
)
}Load more rows
onLoadMore is called when the rows are scrolled close to the end, to fetch the next page. While loading is set, a spinner shows below the rows, or in place of them while there are none. useAsyncList keeps the loaded pages: its load function receives the cursor the previous page returned, and loadMore does nothing once a page returns no cursor. Its items, loading, loadMore, sortDescriptor and sort, and selectedKeys and setSelectedKeys plug into the props of the table. update(key, item), remove(...keys), prepend(...items) and append(...items) change the loaded items without loading them again, for example after a dialog saves a row.
import {
Table,
TableCell,
TableRow,
TableTitle,
useAsyncList
} from 'alinea/components'
interface Product {
id: string
title: string
price: number
}
const materials = ['Linen', 'Oak', 'Wool', 'Walnut', 'Ceramic']
const kinds = ['shirt', 'chair', 'throw', 'stool', 'lamp', 'table']
// Stands in for a paged API: 20 products per page, 100 in total
async function fetchProducts(page: number) {
await new Promise(resolve => setTimeout(resolve, 500))
const items: Array<Product> = Array.from({length: 20}, (_, i) => {
const n = page * 20 + i
const title = `${materials[n % 5]} ${kinds[n % 6]} ${n + 1}`
return {id: `product-${n}`, title, price: 40 + ((n * 37) % 300)}
})
return {items, cursor: page < 4 ? page + 1 : undefined}
}
export function TableAsyncExample() {
const list = useAsyncList<Product, number>({
load: ({cursor = 0}) => fetchProducts(cursor)
})
return (
<div style={{width: 420, height: 260}}>
<Table
aria-label="Products"
items={list.items}
columns={[
{id: 'title', header: 'Title', width: '1fr'},
{id: 'price', header: 'Price', width: 100, align: 'end'}
]}
loading={list.loading}
onLoadMore={list.loadMore}
>
{product => (
<TableRow id={product.id} textValue={product.title}>
<TableTitle title={product.title} />
<TableCell align="end">€{product.price}</TableCell>
</TableRow>
)}
</Table>
</div>
)
}import {useAsyncList} from 'alinea/components'
const list = useAsyncList({
async load({cursor, filterText, sort, signal}) {
const res = await fetch(`/api/products?page=${cursor ?? 1}`, {signal})
const {items, next} = await res.json()
// Leave out the cursor, or return null, on the last page
return {items, cursor: next}
}
})
// list.items, list.loading, list.hasMore, list.error,
// list.loadMore(), list.reload(), list.setFilterText(text), list.sort(sort),
// list.update(id, item), list.remove(id), list.prepend(item)Props
<Table>
toggle (the default) a click runs the row action, or toggles its selection while rows are selected. With replace a click selects only that row and a double click or Enter runs the row action.plain drops the rounded surface, eg. when the table fills a panelselectionBehavior), rows can override it with their own onActionchildren reads besides the item to render them again when those change<TableRow>
onRowActiononAction to act on a single click with the replace selection behavior, where onAction needs a double click.Also accepts data-* attributes.
<TableCell>
Also accepts className and style.
<TableThumbnail>
An image cell, sized to the row height
Also accepts className and style.
<TableTitle>
Also accepts className and style.
useAsyncList()
Loads a list of items, with sorting, filtering and paging
Accessibility
Built on the React Aria grid: the arrow keys move between rows, Space selects a row and Enter runs its action. Sortable column headers announce their sort direction.