Docs
Dynamic Columns
Dynamic Columns
Add, rename, move, delete, reorder, and retype columns at runtime.
Columns become runtime state. useGridColumns owns the list; <DataGridColumns> wires the header menu and add button. New columns render empty cells: no engine changes required.
Feature | Owner | Status |
|---|
1"use client"2
3/**4 * niko-table/grid — dynamic columns (opt-in).5 *6 * `useGridColumns` makes the columns runtime STATE; `<DataGridColumns>` wires7 * the header menu (Rename / Insert left|right / Move / Delete) + the add-column8 * button, and `<DataTableViewDndMenu>` drives show/hide + drag-to-reorder from9 * the same column order. Add / delete / reorder need zero engine or row changes10 * — a missing cell value just renders empty. A fixed-column grid omits this11 * whole block.12 */13import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"14import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"15import { DataTableColumnHideOptions } from "@/components/niko-table/components/data-table-column-hide"16import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"17import { DataTableRowContextMenuSlot } from "@/components/niko-table/components/data-table-row-context-menu-slot"18import { DataTableViewDndMenu } from "@/components/niko-table/components/data-table-view-dnd-menu"19import { DataTable } from "@/components/niko-table/core/data-table"20import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"21import { DataTableRoot } from "@/components/niko-table/core/data-table-root"22import {23 DataTableVirtualizedBody,24 DataTableVirtualizedHeader,25} from "@/components/niko-table/core/data-table-virtualized-structure"26import type { CellEditorProps } from "@/components/niko-table/grid/cells/cell-props"27import { GridTextCell } from "@/components/niko-table/grid/cells/grid-text-cell"28import { DataGridAddColumnButton } from "@/components/niko-table/grid/components/grid-add-column"29import { DataGridClipboard } from "@/components/niko-table/grid/components/grid-clipboard"30import { GridColumnMenuOptions } from "@/components/niko-table/grid/components/grid-column-menu"31import { GridRowMenu } from "@/components/niko-table/grid/components/grid-row-menu"32import {33 DataGridAddRows,34 DataGridToolbar,35} from "@/components/niko-table/grid/components/grid-toolbar"36import { DataGrid } from "@/components/niko-table/grid/core/data-grid"37import { DataGridColumns } from "@/components/niko-table/grid/core/data-grid-columns-context"38import { DataGridCell } from "@/components/niko-table/grid/core/data-grid-context"39import { useDataGrid } from "@/components/niko-table/grid/hooks/use-data-grid"40import {41 useGridColumns,42 type GridColumnSpec,43} from "@/components/niko-table/grid/hooks/use-grid-columns"44import type {45 CellState,46 GridRow,47} from "@/components/niko-table/grid/types/grid-cell"48import type { DataTableColumnDef } from "@/components/niko-table/types"49import * as React from "react"50
51const INITIAL_COLUMNS: GridColumnSpec[] = [52 { id: "feature", label: "Feature", type: "text", width: 200 },53 { id: "owner", label: "Owner", type: "text", width: 160 },54 { id: "status", label: "Status", type: "text", width: 160 },55]56
57const resolveCell = (_columnId: string, raw: string): CellState<string> => ({58 raw,59 value: raw || null,60 status: raw === "" ? "empty" : "valid",61})62
63const c = (raw: string) => resolveCell("", raw)64const INITIAL_ROWS: GridRow[] = [65 { id: "r1", feature: c("Clipboard"), owner: c("Bailey"), status: c("Done") },66 {67 id: "r2",68 feature: c("Fill handle"),69 owner: c("Noah"),70 status: c("In review"),71 },72 {73 id: "r3",74 feature: c("Dynamic columns"),75 owner: c("Mia"),76 status: c("Building"),77 },78]79
80const createEmptyRow = (id: string): GridRow => ({ id })81const gridCellClassName = () =>82 "border-border border-r p-0 align-middle last:border-r-0"83
84export function GridDynamicColumns() {85 const cols = useGridColumns({ initialColumns: INITIAL_COLUMNS })86 const grid = useDataGrid<GridRow>({87 columnIds: cols.columnIds,88 createEmptyRow,89 initialRows: INITIAL_ROWS,90 })91
92 const columns = React.useMemo<DataTableColumnDef<GridRow>[]>(93 () =>94 cols.columns.map(col => ({95 id: col.id,96 accessorFn: (row: GridRow) => {97 const v = row[col.id]98 return typeof v === "object" && v != null ? v.raw : ""99 },100 header: () => (101 <DataTableColumnHeader>102 <DataTableColumnTitle />103 <DataTableColumnActions>104 <DataTableColumnHideOptions />105 {/* Rename / Insert left|right / Move / Delete */}106 <GridColumnMenuOptions />107 </DataTableColumnActions>108 </DataTableColumnHeader>109 ),110 size: col.width,111 meta: { label: col.label },112 cell: ctx => (113 <DataGridCell row={ctx.row.original} columnId={col.id}>114 {(p: CellEditorProps) => (115 <GridTextCell116 {...p}117 resolve={raw => resolveCell(col.id, raw)}118 placeholder={col.label}119 />120 )}121 </DataGridCell>122 ),123 })),124 [cols.columns],125 )126
127 return (128 <DataTableRoot data={grid.rows} columns={columns} getRowId={r => r.id}>129 <DataGrid grid={grid} className="space-y-2 outline-none">130 {/* Flex-fill + drag-resizable columns */}131 <DataTableColumnResize />132 <DataGridClipboard resolveCell={resolveCell} />133 <DataGridColumns value={cols}>134 <DataGridToolbar>135 <DataGridAddRows count={3} />136 <DataGridAddColumnButton />137 <DataTableViewDndMenu138 columnOrder={cols.columnIds}139 onColumnOrderChange={cols.reorderColumns}140 />141 </DataGridToolbar>142 <DataTable maxHeight={360}>143 <DataTableVirtualizedHeader />144 <DataTableVirtualizedBody<GridRow>145 estimateSize={37}146 fixedRowHeight147 getCellClassName={gridCellClassName}148 >149 <DataTableRowContextMenuSlot>150 <GridRowMenu />151 </DataTableRowContextMenuSlot>152 </DataTableVirtualizedBody>153 </DataTable>154 </DataGridColumns>155 </DataGrid>156 </DataTableRoot>157 )158}Preview with Controlled State
Feature | Owner | Status |
|---|
Current Grid State
Live view of the grid engine state for demonstration
Focused Cell:None
Editing Cell:None
Selection Anchor:None
Total Rows:3
Column Count:3
Column Ids:feature, owner, status
Can Undo / Redo:No / No
Last Commit:None
1"use client"2
3/**4 * niko-table/grid — dynamic columns with a live state panel for docs.5 */6import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"7import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"8import { DataTableColumnHideOptions } from "@/components/niko-table/components/data-table-column-hide"9import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"10import { DataTableRowContextMenuSlot } from "@/components/niko-table/components/data-table-row-context-menu-slot"11import { DataTableViewDndMenu } from "@/components/niko-table/components/data-table-view-dnd-menu"12import { DataTable } from "@/components/niko-table/core/data-table"13import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"14import { DataTableRoot } from "@/components/niko-table/core/data-table-root"15import {16 DataTableVirtualizedBody,17 DataTableVirtualizedHeader,18} from "@/components/niko-table/core/data-table-virtualized-structure"19import type { CellEditorProps } from "@/components/niko-table/grid/cells/cell-props"20import { GridTextCell } from "@/components/niko-table/grid/cells/grid-text-cell"21import { DataGridAddColumnButton } from "@/components/niko-table/grid/components/grid-add-column"22import { DataGridClipboard } from "@/components/niko-table/grid/components/grid-clipboard"23import { GridColumnMenuOptions } from "@/components/niko-table/grid/components/grid-column-menu"24import { GridRowMenu } from "@/components/niko-table/grid/components/grid-row-menu"25import {26 DataGridAddRows,27 DataGridToolbar,28} from "@/components/niko-table/grid/components/grid-toolbar"29import { DataGrid } from "@/components/niko-table/grid/core/data-grid"30import { DataGridColumns } from "@/components/niko-table/grid/core/data-grid-columns-context"31import { DataGridCell } from "@/components/niko-table/grid/core/data-grid-context"32import { useDataGrid } from "@/components/niko-table/grid/hooks/use-data-grid"33import {34 useGridColumns,35 type GridColumnSpec,36} from "@/components/niko-table/grid/hooks/use-grid-columns"37import type {38 CellState,39 GridRow,40} from "@/components/niko-table/grid/types/grid-cell"41import type { DataTableColumnDef } from "@/components/niko-table/types"42import { Button } from "@/components/ui/button"43import {44 Card,45 CardAction,46 CardContent,47 CardDescription,48 CardHeader,49 CardTitle,50} from "@/components/ui/card"51import {52 Collapsible,53 CollapsibleContent,54 CollapsibleTrigger,55} from "@/components/ui/collapsible"56import { ChevronRight } from "lucide-react"57import * as React from "react"58
59const INITIAL_COLUMNS: GridColumnSpec[] = [60 { id: "feature", label: "Feature", type: "text", width: 200 },61 { id: "owner", label: "Owner", type: "text", width: 160 },62 { id: "status", label: "Status", type: "text", width: 160 },63]64
65const resolveCell = (_columnId: string, raw: string): CellState<string> => ({66 raw,67 value: raw || null,68 status: raw === "" ? "empty" : "valid",69})70
71const c = (raw: string) => resolveCell("", raw)72const INITIAL_ROWS: GridRow[] = [73 { id: "r1", feature: c("Clipboard"), owner: c("Bailey"), status: c("Done") },74 {75 id: "r2",76 feature: c("Fill handle"),77 owner: c("Noah"),78 status: c("In review"),79 },80 {81 id: "r3",82 feature: c("Dynamic columns"),83 owner: c("Mia"),84 status: c("Building"),85 },86]87
88const createEmptyRow = (id: string): GridRow => ({ id })89const gridCellClassName = () =>90 "border-border border-r p-0 align-middle last:border-r-0"91
92function formatPos(93 pos: { rowId: string; columnId: string } | null | undefined,94): string {95 if (!pos) return "None"96 return `${pos.rowId} / ${pos.columnId}`97}98
99export function GridDynamicColumnsState() {100 const [resetKey, setResetKey] = React.useState(0)101 return (102 <GridDynamicColumnsStateInner103 key={resetKey}104 onReset={() => setResetKey(k => k + 1)}105 />106 )107}108
109function GridDynamicColumnsStateInner({ onReset }: { onReset: () => void }) {110 const cols = useGridColumns({ initialColumns: INITIAL_COLUMNS })111 const grid = useDataGrid<GridRow>({112 columnIds: cols.columnIds,113 createEmptyRow,114 initialRows: INITIAL_ROWS,115 })116
117 const columns = React.useMemo<DataTableColumnDef<GridRow>[]>(118 () =>119 cols.columns.map(col => ({120 id: col.id,121 accessorFn: (row: GridRow) => {122 const v = row[col.id]123 return typeof v === "object" && v != null ? v.raw : ""124 },125 header: () => (126 <DataTableColumnHeader>127 <DataTableColumnTitle />128 <DataTableColumnActions>129 <DataTableColumnHideOptions />130 <GridColumnMenuOptions />131 </DataTableColumnActions>132 </DataTableColumnHeader>133 ),134 size: col.width,135 meta: { label: col.label },136 cell: ctx => (137 <DataGridCell row={ctx.row.original} columnId={col.id}>138 {(p: CellEditorProps) => (139 <GridTextCell140 {...p}141 resolve={raw => resolveCell(col.id, raw)}142 placeholder={col.label}143 />144 )}145 </DataGridCell>146 ),147 })),148 [cols.columns],149 )150
151 return (152 <div className="w-full space-y-4">153 <DataTableRoot data={grid.rows} columns={columns} getRowId={r => r.id}>154 <DataGrid grid={grid} className="space-y-2 outline-none">155 {/* Flex-fill + drag-resizable columns */}156 <DataTableColumnResize />157 <DataGridClipboard resolveCell={resolveCell} />158 <DataGridColumns value={cols}>159 <DataGridToolbar>160 <DataGridAddRows count={3} />161 <DataGridAddColumnButton />162 <DataTableViewDndMenu163 columnOrder={cols.columnIds}164 onColumnOrderChange={cols.reorderColumns}165 />166 </DataGridToolbar>167 <DataTable maxHeight={360}>168 <DataTableVirtualizedHeader />169 <DataTableVirtualizedBody<GridRow>170 estimateSize={37}171 fixedRowHeight172 getCellClassName={gridCellClassName}173 >174 <DataTableRowContextMenuSlot>175 <GridRowMenu />176 </DataTableRowContextMenuSlot>177 </DataTableVirtualizedBody>178 </DataTable>179 </DataGridColumns>180 </DataGrid>181 </DataTableRoot>182
183 <Card>184 <CardHeader>185 <CardTitle>Current Grid State</CardTitle>186 <CardDescription>187 Live view of the grid engine state for demonstration188 </CardDescription>189 <CardAction>190 <Button variant="outline" size="sm" onClick={onReset}>191 Reset All State192 </Button>193 </CardAction>194 </CardHeader>195 <CardContent className="space-y-4">196 <div className="grid gap-2 text-xs text-muted-foreground">197 <div className="flex justify-between">198 <span className="font-medium">Focused Cell:</span>199 <span className="text-foreground">200 {formatPos(grid.focusedCell)}201 </span>202 </div>203 <div className="flex justify-between">204 <span className="font-medium">Editing Cell:</span>205 <span className="text-foreground">206 {formatPos(grid.editingCell)}207 </span>208 </div>209 <div className="flex justify-between">210 <span className="font-medium">Selection Anchor:</span>211 <span className="text-foreground">212 {formatPos(grid.selectionAnchor)}213 </span>214 </div>215 <div className="flex justify-between">216 <span className="font-medium">Total Rows:</span>217 <span className="text-foreground">{grid.rows.length}</span>218 </div>219 <div className="flex justify-between">220 <span className="font-medium">Column Count:</span>221 <span className="text-foreground">{cols.columnIds.length}</span>222 </div>223 <div className="flex justify-between gap-4">224 <span className="shrink-0 font-medium">Column Ids:</span>225 <span className="truncate text-end text-foreground">226 {cols.columnIds.join(", ")}227 </span>228 </div>229 <div className="flex justify-between">230 <span className="font-medium">Can Undo / Redo:</span>231 <span className="text-foreground">232 {grid.canUndo ? "Yes" : "No"} / {grid.canRedo ? "Yes" : "No"}233 </span>234 </div>235 <div className="flex justify-between">236 <span className="font-medium">Last Commit:</span>237 <span className="text-foreground">238 {grid.lastCommit239 ? `${grid.lastCommit.kind} (seq ${grid.lastCommit.seq})`240 : "None"}241 </span>242 </div>243 </div>244
245 <Collapsible>246 <CollapsibleTrigger className="flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground [&[data-state=open]>svg]:rotate-90">247 <ChevronRight className="size-3.5 transition-transform" />248 View Full State Object249 </CollapsibleTrigger>250 <CollapsibleContent>251 <pre className="mt-2 max-h-64 overflow-auto rounded-md bg-muted p-3 text-[10px] leading-relaxed">252 {JSON.stringify(253 {254 focusedCell: grid.focusedCell,255 editingCell: grid.editingCell,256 selectionAnchor: grid.selectionAnchor,257 canUndo: grid.canUndo,258 canRedo: grid.canRedo,259 lastCommit: grid.lastCommit,260 rowCount: grid.rows.length,261 columnCount: cols.columnIds.length,262 columnIds: cols.columnIds,263 columns: cols.columns,264 rows: grid.rows,265 },266 null,267 2,268 )}269 </pre>270 </CollapsibleContent>271 </Collapsible>272 </CardContent>273 </Card>274 </div>275 )276}Installation
Section titled “Installation”pnpm dlx shadcn@latest add @niko-table/data-table-grid @niko-table/data-table-view-dnd-menu1const cols = useGridColumns({2 initialColumns: [3 { id: "task", label: "Task", type: "text" },4 { id: "status", label: "Status", type: "select", options: ["Todo", "Done"] },5 ],6})7const grid = useDataGrid({ columnIds: cols.columnIds, createEmptyRow })Header menu:
1<DataTableColumnActions>2 <DataTableColumnHideOptions />3 <GridColumnMenuOptions />4</DataTableColumnActions>Provider + toolbar:
1<DataGrid grid={grid}>2 <DataGridColumns value={cols}>3 <DataGridToolbar>4 <DataGridAddColumnButton />5 <DataTableViewDndMenu6 columnOrder={cols.columnIds}7 onColumnOrderChange={cols.reorderColumns}8 />9 </DataGridToolbar>10 {/* table… */}11 </DataGridColumns>12</DataGrid>API: columns, columnIds, addColumn, renameColumn, moveColumn, removeColumn, reorderColumns, setColumnType.
Retype
Section titled “Retype”1<DataGridColumns2 value={cols}3 columnTypes={[4 { value: "text", label: "Text" },5 { value: "select", label: "Select", options: STATUSES },6 ]}7 resolveCell={resolveCell}8>Existing cells re-resolve against the new type (unmatched values turn invalid).