UNPKG

@loke/design-system

Version:

A design system with individually importable components

465 lines (395 loc) 13 kB
--- name: data-tables description: > Build data table views with TanStack Table + design system Table component. Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption. ColumnDef object definitions with custom cell renderers using DS components. Sortable headers with Button variant="ghost" + sort icons. Row selection with Checkbox and data-state="selected" attribute. flexRender for header/cell content. Infinite scroll with TanStack Query. Reusable DataTable wrapper pattern. Activate when building data tables with sorting, filtering, pagination, or row selection. type: composition library: '@loke/design-system' library_version: '2.0.0-rc.6' requires: - getting-started - interactive-components sources: - 'LOKE/merchant-frontends:packages/design-system/src/components/table' - 'LOKE/merchant-frontends:packages/design-system/src/components/pagination' - 'LOKE/merchant-frontends:apps/office/src/components/data-table/index.tsx' - 'LOKE/merchant-frontends:apps/office/src/components/data-table/standard-table.tsx' - 'LOKE/merchant-frontends:apps/office/src/components/data-table/rows.tsx' --- # Data Tables This skill builds on **getting-started** and **interactive-components**. Read them first. ## Setup Install TanStack Table alongside the design system: ```bash bun add @tanstack/react-table ``` Minimal integration connecting TanStack Table to DS Table components: ```tsx import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@loke/design-system/table"; import { type ColumnDef, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; interface Payment { id: string; amount: number; status: "pending" | "completed" | "failed"; } const columns: ColumnDef<Payment>[] = [ { accessorKey: "id", header: "ID" }, { accessorKey: "amount", header: "Amount" }, { accessorKey: "status", header: "Status" }, ]; function PaymentsTable({ data }: { data: Payment[] }) { const table = useReactTable({ columns, data, getCoreRowModel: getCoreRowModel(), }); return ( <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => ( <TableHead key={header.id}> {flexRender( header.column.columnDef.header, header.getContext(), )} </TableHead> ))} </TableRow> ))} </TableHeader> <TableBody> {table.getRowModel().rows.map((row) => ( <TableRow key={row.id}> {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </TableCell> ))} </TableRow> ))} </TableBody> </Table> ); } ``` ## Core Patterns ### Table component composition The DS Table maps directly to semantic HTML table elements. Always nest them correctly: ``` Table TableHeader TableRow TableHead (one per column) TableBody TableRow (one per data row) TableCell (one per column) TableFooter (optional) TableRow TableCell TableCaption (optional, direct child of Table) ``` The `Table` component wraps itself in a `div.relative.w-full.overflow-auto` for horizontal scroll on small screens. `TableRow` includes built-in `hover:bg-muted/50` and `data-[state=selected]:bg-muted` styles. ### ColumnDef with custom cell renderers Use DS components inside `cell` functions. Always use `flexRender` for both headers and cells: ```tsx import { Button } from "@loke/design-system/button"; import type { ColumnDef } from "@tanstack/react-table"; const columns: ColumnDef<Discount>[] = [ { accessorKey: "name", cell: ({ row }) => ( <div className="min-w-0"> <div className="truncate font-medium text-sm"> {row.original.name} </div> </div> ), header: "Discount", minSize: 200, }, { accessorKey: "status", cell: ({ row }) => <DiscountStatusBadge status={row.original.status} />, header: "Status", size: 120, }, { accessorFn: (d) => d.id, cell: ({ row }) => ( <Button className="size-10 rounded-full" onClick={(e) => { e.stopPropagation(); handleRowClick(row.original); }} size="icon" variant="ghost" > <ArrowRight size="md" /> </Button> ), enableSorting: false, header: "", id: "actions", size: 40, }, ]; ``` Use `accessorKey` for direct property access, `accessorFn` for computed values. Set `id` explicitly when using `accessorFn`. Control column widths with `size`, `minSize`, and `maxSize`. ### Sortable headers The office app pattern uses `Button variant="ghost"` with sort icons from `@loke/icons`. This is implemented in `StandardTable`: ```tsx import { Button } from "@loke/design-system/button"; import { TableHead, TableRow } from "@loke/design-system/table"; import { ChevronsUpDownIcon, SortAscIcon, SortDescIcon } from "@loke/icons"; import { flexRender } from "@tanstack/react-table"; // Inside header rendering: {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { const canSort = header.column.getCanSort(); const sortDirection = header.column.getIsSorted(); if (canSort) { return ( <TableHead key={header.id}> <Button className="w-full justify-between py-1.5" onClick={header.column.getToggleSortingHandler()} size="sm" variant="ghost" > <span> {flexRender( header.column.columnDef.header, header.getContext(), )} </span> <span> {sortDirection === "asc" && ( <SortAscIcon className="text-muted-foreground" /> )} {sortDirection === "desc" && ( <SortDescIcon className="text-muted-foreground" /> )} {!sortDirection && ( <ChevronsUpDownIcon className="text-muted-foreground" /> )} </span> </Button> </TableHead> ); } return ( <TableHead key={header.id}> {flexRender( header.column.columnDef.header, header.getContext(), )} </TableHead> ); })} </TableRow> ))} ``` Configure sorting state on `useReactTable`: ```tsx import { type SortingState, getSortedRowModel } from "@tanstack/react-table"; import { useState } from "react"; const [sorting, setSorting] = useState<SortingState>([]); const table = useReactTable({ columns, data, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, state: { sorting }, }); ``` Disable sorting on specific columns with `enableSorting: false` in the column def. ### Row selection Add a checkbox column as the first column. Use `data-state` on `TableRow` so the DS built-in `data-[state=selected]:bg-muted` style applies: ```tsx import { Checkbox } from "@loke/design-system/checkbox"; import type { ColumnDef } from "@tanstack/react-table"; const selectionColumn: ColumnDef<MyData> = { accessorKey: "isChecked", cell: ({ row }) => ( <div className="flex items-center justify-center"> <Checkbox checked={Boolean(row.original.isChecked)} onCheckedChange={(v) => { toggleSelection(row.original.id, Boolean(v)); }} onClick={(e) => e.stopPropagation()} /> </div> ), enableColumnFilter: false, enableSorting: false, header: "", minSize: 40, size: 40, }; ``` On the row, set `data-state` for styling: ```tsx <TableRow data-state={row.getIsSelected() && "selected"} onClick={(event) => handleRowClick(row, event)} style={{ cursor: hasSelectableRows ? "pointer" : "default" }} > {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </TableCell> ))} </TableRow> ``` ### Row actions with DropdownMenu Add an actions column using DropdownMenu for contextual operations: ```tsx import { Button } from "@loke/design-system/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@loke/design-system/dropdown-menu"; import { EllipsisVertical } from "@loke/icons"; const actionsColumn: ColumnDef<MyData> = { cell: ({ row }) => ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button size="icon" variant="ghost"> <EllipsisVertical /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => handleEdit(row.original)}> Edit </DropdownMenuItem> <DropdownMenuItem onClick={() => handleDelete(row.original)}> Delete </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ), enableSorting: false, header: "", id: "actions", size: 40, }; ``` Always use `e.stopPropagation()` on action buttons when rows are clickable. ### Reusable DataTable wrapper The office app uses a generic `DataTable` wrapper (`apps/office/src/components/data-table/index.tsx`) that accepts `columns` and `data` generically, with optional props for sorting, row selection, grouping, infinite scroll, toolbars, and empty state placeholders. It internally manages `useReactTable` state and delegates rendering to `StandardTable` (semantic `<table>`) or `InfiniteScrollTable` (virtualized with `@tanstack/react-virtual`). Key props: `sorting` and `rowSelection` accept objects with both state and `OnChangeFn` callbacks, allowing either controlled (server-driven) or uncontrolled (local) modes. `infiniteScroll` takes `fetchNextPage`, `hasNextPage`, and `isFetching` from TanStack Query's `useInfiniteQuery`. Usage: ```tsx <DataTable columns={columns} data={tableData} getRowId={(row) => row.uid} infiniteScroll={{ fetchNextPage: onFetchNextPage, hasNextPage: !reachedEnd, isFetching: isLoadingCustomers && !reachedEnd, }} rowSelection={{ onRowSelectionChange, rowSelection }} sorting={{ onSortingChange, sorting }} /> ``` ## Common Mistakes ### HIGH: Incorrect semantic table nesting Wrong -- `TableCell` inside `TableHeader`, or `TableHead` inside `TableBody`: ```tsx // WRONG <TableHeader> <TableRow> <TableCell>Name</TableCell> </TableRow> </TableHeader> <TableBody> <TableRow> <TableHead>John</TableHead> </TableRow> </TableBody> ``` Correct -- `TableHead` in header, `TableCell` in body: ```tsx // CORRECT <TableHeader> <TableRow> <TableHead>Name</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow> <TableCell>John</TableCell> </TableRow> </TableBody> ``` `TableHead` renders `<th>` (with muted foreground, medium font weight). `TableCell` renders `<td>`. Mixing them breaks accessibility and styling. ### MEDIUM: Not using data-state for selected row styling Wrong -- conditional className for selected rows: ```tsx // WRONG <TableRow className={row.getIsSelected() ? "bg-muted" : ""}> ``` Correct -- use the `data-state` attribute. The DS `TableRow` already includes `data-[state=selected]:bg-muted`: ```tsx // CORRECT <TableRow data-state={row.getIsSelected() && "selected"}> ``` This keeps styling consistent with the design system and avoids duplicating or conflicting with built-in styles. ### MEDIUM: Building sort UI from scratch Wrong -- custom sort icon toggle logic with manual state: ```tsx // WRONG <TableHead onClick={() => setSort(col)}> {sortCol === col ? (sortDir === "asc" ? "^" : "v") : "-"} </TableHead> ``` Correct -- use `Button variant="ghost"` with TanStack Table's built-in sort handler and `@loke/icons` sort icons: ```tsx // CORRECT <TableHead> <Button className="w-full justify-between py-1.5" onClick={header.column.getToggleSortingHandler()} size="sm" variant="ghost" > <span>{flexRender(header.column.columnDef.header, header.getContext())}</span> <span> {sortDirection === "asc" && <SortAscIcon className="text-muted-foreground" />} {sortDirection === "desc" && <SortDescIcon className="text-muted-foreground" />} {!sortDirection && <ChevronsUpDownIcon className="text-muted-foreground" />} </span> </Button> </TableHead> ``` The three icons are `SortAscIcon`, `SortDescIcon`, and `ChevronsUpDownIcon` from `@loke/icons`. ## See also - **interactive-components/SKILL.md** -- Button, Checkbox components used in tables - **overlay-composition/SKILL.md** -- DropdownMenu for row actions, Dialog for table actions