Spreadsheet recipe
Custom controls
Combine find/replace, Go To Cell, formatting, validation, pagination, and controlled columns.
ControlledFormattingPagination
Interactive, production component
tsx
import { useMemo, useState } from "react";
import { DataTable } from "../../Datatable";
import type {
ColumnLayoutSettings,
ColumnSetting,
ValidationContext,
} from "../../Datatable";
import { products as initialProducts, type ProductRow } from "./exampleData";
import type { ExampleProps } from "./types";
function makeColumns(showOps: boolean): ColumnSetting<ProductRow>[] {
return [
{ id: "id", title: "ID", width: 82, pinned: "left", align: "right" },
{ id: "sku", title: "SKU", width: 116 },
{
id: "product",
title: "Product",
width: 210,
editor: "text",
validation: ({ rowValue, z: zod }: ValidationContext<ProductRow>) =>
rowValue.status === "Draft"
? zod.string().min(8, "Draft product names need more detail")
: zod.string().min(3, "Use at least 3 characters"),
},
{ id: "brand", title: "Brand", width: 110 },
{ id: "owner", title: "Owner", width: 150, visible: showOps },
{
id: "stock",
title: "Stock",
width: 96,
align: "right",
editor: { type: "number", min: 0, step: 1 },
validation: ({ rowValue, z: zod }: ValidationContext<ProductRow>) =>
rowValue.featured
? zod.coerce.number().min(20, "Featured items need at least 20 stock")
: zod.coerce.number().min(0),
},
{ id: "price", title: "Price", width: 110, align: "right", format: { type: "currency", currency: "USD", decimals: 2 } },
{ id: "margin", title: "Margin", width: 110, align: "right", format: { type: "number", decimals: 1 } },
{ id: "updatedAt", title: "Updated", width: 126, format: { type: "date", dateStyle: "medium" } },
{ id: "status", title: "Status", width: 112, pinned: "right" },
];
}
function makeLayout(compact: boolean, showOps: boolean): ColumnLayoutSettings {
return {
order: compact
? ["id", "product", "status", "stock", "price", "margin", "sku", "brand", "owner", "updatedAt"]
: ["id", "sku", "product", "brand", "owner", "stock", "price", "margin", "updatedAt", "status"],
visibility: {
owner: showOps,
brand: !compact,
updatedAt: !compact,
},
pinning: { left: ["id"], right: ["status"] },
sizing: compact ? { product: 240, status: 104, stock: 88 } : { product: 210 },
alignment: { id: "right", stock: "right", price: "right", margin: "right" },
formatting: {
price: { type: "currency", currency: "USD", decimals: 2 },
margin: { type: "number", decimals: 1 },
updatedAt: { type: "date", dateStyle: "medium" },
},
};
}
export default function CustomControlsExample({ theme }: ExampleProps) {
const [rows, setRows] = useState(initialProducts);
const [page, setPage] = useState({ pageIndex: 0, pageSize: 25 });
const [compact, setCompact] = useState(false);
const [showOps, setShowOps] = useState(true);
const columns = useMemo(() => makeColumns(showOps), [showOps]);
const layout = useMemo(() => makeLayout(compact, showOps), [compact, showOps]);
const pageCount = Math.max(1, Math.ceil(rows.length / page.pageSize));
return (
<div>
<div className="example-demo-toolbar" aria-describedby="custom-controls-help">
<p id="custom-controls-help" className="example-demo-toolbar__help">
External page, layout, and Owner column stay wired while the table keeps editing and formatting.
</p>
<div className="example-demo-toolbar__actions" role="group" aria-label="External table controls">
<button
type="button"
title="Previous page"
disabled={page.pageIndex <= 0}
onClick={() => setPage((current) => ({ ...current, pageIndex: Math.max(0, current.pageIndex - 1) }))}
>
Prev
</button>
<button
type="button"
title="Next page"
disabled={page.pageIndex >= pageCount - 1}
onClick={() => setPage((current) => ({ ...current, pageIndex: Math.min(pageCount - 1, current.pageIndex + 1) }))}
>
Next
</button>
<span className="example-demo-toolbar__meta" aria-live="polite">
Page {page.pageIndex + 1}/{pageCount}
</span>
<button
type="button"
className={compact ? "is-active" : undefined}
title="Toggle compact column layout"
aria-pressed={compact}
onClick={() => setCompact((value) => !value)}
>
Layout
</button>
<button
type="button"
className={showOps ? "is-active" : undefined}
title="Toggle owner column visibility"
aria-pressed={showOps}
onClick={() => setShowOps((value) => !value)}
>
Owner col
</button>
</div>
</div>
<DataTable
theme={theme}
mode="table"
dataSource={rows}
onChange={setRows}
columnSettings={columns}
columnLayoutSettings={layout}
maxHeight={520}
showGlobalSearch
showColumnMenus
showFooter
enableEditing
enableRowSelection
enableRowActions
enableColumnResizing
enableColumnReorder
enableColumnSettings
enableFindReplace
enableGoToCell
enablePagination
pagination={page}
onPaginationChange={setPage}
conditionalFormatting={[
{
columns: ["stock"],
when: ({ row }) => row.stock < row.reorderAt,
style: { backgroundColor: "#fef3c7", color: "#92400e", fontWeight: 700 },
},
{
columns: ["status"],
when: ({ value }) => value === "Paused",
style: { backgroundColor: "#fee2e2", color: "#991b1b" },
},
]}
rowStyle={({ row }) =>
row.featured ? { backgroundColor: "rgba(250, 204, 21, 0.12)" } : undefined
}
/>
</div>
);
}
Related API