@alphinex/table
v1.2.1
Published
Enterprise DataGrid: headless engine + styled renderer.
Readme
@alphinex/table
Enterprise DataGrid: a headless useDataGrid() engine — a thin, opinionated wrapper over
TanStack Table (see ADR-0003) — plus <DataGrid>, the styled
renderer built on top of it, complete with toolbar, pagination, column-visibility, and row
selection. useDataGrid() returns the real TanStack Table<TData> instance, so anything
TanStack Table exposes is available if you want to build fully custom rendering instead of
using <DataGrid>.
useDataGrid
The headless engine. Give it data and columns (standard TanStack ColumnDef[]) and it
manages sorting, column filters, a global filter, pagination, row selection, and column
visibility as internal state, wiring them into a useReactTable() instance:
import { useDataGrid, type ColumnDef } from "@alphinex/table";
import { flexRender } from "@tanstack/react-table";
interface Invoice {
id: string;
customer: string;
amount: number;
}
const columns: ColumnDef<Invoice, unknown>[] = [
{ accessorKey: "id", header: "Invoice" },
{ accessorKey: "customer", header: "Customer" },
{ accessorKey: "amount", header: "Amount" },
];
function CustomInvoiceTable({ data }: { data: Invoice[] }) {
const table = useDataGrid({ data, columns, defaultPageSize: 25 });
return (
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((header) => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Key options (UseDataGridOptions<TData>):
mode?: "client" | "server"(default"client") — in"server"mode, sorting/filtering/ pagination stop running locally (manualSorting/manualFiltering/manualPagination) andonQueryChangefires with the current{ sorting, columnFilters, globalFilter, pagination }whenever any of them changes, so the app can refetch.rowCount?: number— total row count across all pages; required for correct pagination math in"server"mode.selection?: "none" | "single" | "multiple"(default"none") — enables/limits TanStack's row-selection state.getRowId?: (row, index) => stringanddefaultPageSize?: number(default10).
DataGrid
The styled renderer: everything useDataGrid() leaves as an implementation detail, wired up
with @alphinex/ui primitives and semantic tokens. It renders as an ARIA grid over divs
(not a native <table>, so rows can be virtualized reliably), auto-virtualizes once there are
more than ~50 rows, and includes a toolbar and pagination footer by default:
import { DataGrid } from "@alphinex/table";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@alphinex/ui";
import { formatCurrency } from "@alphinex/utils";
interface Invoice {
id: string;
customer: string;
amount: number;
status: "paid" | "pending" | "overdue";
}
const columns: ColumnDef<Invoice, unknown>[] = [
{ accessorKey: "id", header: "Invoice" },
{ accessorKey: "customer", header: "Customer" },
{
accessorKey: "amount",
header: "Amount",
cell: ({ getValue }) => formatCurrency(getValue<number>()),
},
{
accessorKey: "status",
header: "Status",
cell: ({ getValue }) => <Badge size="sm">{getValue<string>()}</Badge>,
},
];
function InvoiceGrid({ invoices }: { invoices: Invoice[] }) {
return (
<DataGrid
data={invoices}
columns={columns}
selection="multiple"
defaultPageSize={10}
searchPlaceholder="Search invoices…"
/>
);
}DataGridProps<TData> extends UseDataGridOptions<TData> and adds rendering concerns:
isLoading? (renders skeleton rows), emptyState? (node shown for zero rows), virtualized?
(force on/off — default is auto above the 50-row threshold), maxHeight? (scroll-container
height while virtualized, default "24rem"), toolbar? (pass false to hide it, or a node to
replace the default DataGridToolbar), searchPlaceholder?, pageSizeOptions?, and
showPagination? (pass false when the app paginates externally).
Column sorting comes for free: any column without enableSorting: false gets a clickable
header with a sort-direction chevron, driven by TanStack's getToggleSortingHandler().
DataGridToolbar, DataGridPagination, ColumnVisibilityMenu
These are the pieces <DataGrid> composes internally — exported individually so you can reuse
them in a custom layout (e.g. next to useDataGrid()), or pass your own toolbar node to
<DataGrid> while keeping the default pagination:
import {
useDataGrid,
DataGridToolbar,
DataGridPagination,
ColumnVisibilityMenu,
} from "@alphinex/table";
function CustomToolbarGrid({
data,
columns,
}: {
data: Invoice[];
columns: ColumnDef<Invoice, unknown>[];
}) {
const table = useDataGrid({ data, columns });
return (
<div>
<DataGridToolbar table={table} searchPlaceholder="Filter…" />
{/* custom row rendering using `table` */}
<DataGridPagination table={table} pageSizeOptions={[10, 25, 50]} />
</div>
);
}DataGridToolbar renders a global-filter Input plus ColumnVisibilityMenu (a minimal
anchored dropdown — not the general-purpose @alphinex/ui Menu, per ADR-0001 — that lists any
column with enableHiding not disabled and toggles it). DataGridPagination renders the
page-size Select, page-count label, and previous/next buttons, driven by table.previousPage()
/ table.nextPage() / table.setPageSize().
createSelectionColumn
Builds the checkbox column <DataGrid> prepends when selection isn't "none". It lives
outside useDataGrid() deliberately: headless consumers building fully custom rendering add
their own selection column (or don't) rather than getting one automatically.
import { createSelectionColumn } from "@alphinex/table";
const selectionColumn = createSelectionColumn<Invoice>("multiple"); // or "single" / "none" (→ null)
const columnsWithSelection = selectionColumn ? [selectionColumn, ...columns] : columns;CSV export
DataGridToolbar shows an "Export CSV" button by default (enableCsvExport={false} to hide it,
csvFilename to name the download — both also available as <DataGrid> props). It exports the
currently filtered/sorted rows for the currently visible columns, not just the current page:
<DataGrid data={invoices} columns={columns} csvFilename="invoices.csv" />The underlying pieces are exported too, if you want a custom export trigger:
exportTableToCsv(table, filename?) (downloads directly) or tableToCsv(table) (just returns
the CSV string).
CSV import — CsvImportDialog
A Dialog composite (Sprint 27) for CSV import with inline, per-row validation — see
ADR-0020 (CSV only, no
.xlsx support yet):
import { CsvImportDialog } from "@alphinex/table";
import { z } from "zod";
const contactSchema = z.object({
Name: z.string().min(1, "Name is required"),
Role: z.enum(["admin", "member"]),
});
<CsvImportDialog
isOpen={isImportOpen}
onClose={() => setIsImportOpen(false)}
schema={contactSchema}
onImport={(rows) => saveContacts(rows)} // only the rows that passed validation
/>;The schema's field names must match the CSV's header row. Rows that fail schema.safeParse()
are listed inline with their row number and error message and excluded from onImport — this
package has no persistence opinion, onImport receives validated data and the app decides where
it goes.
Column reorder
Drag any column's grab handle in the ColumnVisibilityMenu ("Columns" button in the default
toolbar) to reorder it — uses @dnd-kit/sortable, the same library @alphinex/kanban already
adopted (ADR-0011), wired through TanStack Table's columnOrder state. No extra setup needed —
it's part of useDataGrid()/<DataGrid> already.
See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.
