gridcn
exampledata-grid-cell-errors-demosource: ok · hit
Data Grid Cell Errors Demo
A fake saveOrders rejects any committed Quantity over 100 after ~600ms; the onDataChange handler calls actions.setCellErrors to paint the exact cell with the same ring/tint/aria-invalid treatment as a live validate rejection, after commit. A badge reads the count via useDataGridCellErrors(), and a Clear all button calls clearCellErrors().
npx shadcn add "https://gridcn.vercel.app/r/data-grid-cell-errors-demo.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
DammersCode/gridcn/data-gridDammersCode/gridcn/data-grid-history
Files1 · source cached 10/15/2026, 4:51:24 AM
registry/default/examples/data-grid-cell-errors-demo.tsx
"use client";
import { type ReactNode } from "react";
import {
DataGridBody,
DataGridHeader,
DataGridProvider,
DataGridRoot,
defineColumns,
useDataGridActions,
useDataGridCellErrors,
useDataGridStoreProps,
type DataChange,
type DataOp,
} from "@/registry/default/components/data-grid/data-grid";
import { useDataGridState } from "@/registry/default/components/data-grid-history/data-grid-history";
import { Button } from "@/components/ui/button";
type Order = { id: string; item: string; quantity: number; status: string };
const columns = defineColumns<Order>()([
{ id: "item", header: "Item", accessorKey: "item", type: "text", width: 150, flex: 2 },
{ id: "quantity", header: "Quantity", accessorKey: "quantity", type: "number", width: 110, flex: 1 },
{ id: "status", header: "Status", accessorKey: "status", type: "text", width: 100, flex: 1 },
] as const);
function initialRows(): Order[] {
return [
{ id: "row-0", item: "Widget", quantity: 12, status: "ok" },
{ id: "row-1", item: "Gadget", quantity: 40, status: "ok" },
{ id: "row-2", item: "Gizmo", quantity: 8, status: "ok" },
{ id: "row-3", item: "Doohickey", quantity: 25, status: "ok" },
{ id: "row-4", item: "Thingamajig", quantity: 60, status: "ok" },
];
}
const getRowId = (row: Order) => row.id;
type RejectedCell = { rowId: string; columnId: string };
function isUpdateWithCells(op: DataOp<Order>): op is Extract<DataOp<Order>, { type: "update" }> & {
cells: NonNullable<Extract<DataOp<Order>, { type: "update" }>["cells"]>;
} {
return op.type === "update" && Boolean(op.cells);
}
/** Fake server call: rejects any committed quantity over 100, after ~600ms. */
function saveOrders(ops: DataChange<Order>["ops"]): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
const tooMany: RejectedCell[] = ops
.filter(isUpdateWithCells)
.flatMap((op) =>
op.cells
.filter((cell) => cell.columnId === "quantity" && typeof cell.value === "number" && cell.value > 100)
.map((cell) => ({ rowId: op.rowId, columnId: cell.columnId })),
);
if (tooMany.length > 0) reject(tooMany);
else resolve();
}, 600);
});
}
/** Reads the live cellErrors map from inside the provider and drives the badge + clear button. */
function CellErrorsBar(): ReactNode {
const actions = useDataGridActions();
const cellErrors = useDataGridCellErrors();
return (
<div className="flex items-center gap-3 text-sm">
<span className="text-muted-foreground">
Server errors: <span className="font-medium text-foreground">{cellErrors.size}</span>
</span>
<Button size="sm" variant="outline" disabled={cellErrors.size === 0} onClick={() => actions.clearCellErrors()}>
Clear all
</Button>
</div>
);
}
/**
* Edit a Quantity cell past 100 and commit: `onDataChange` calls a fake `saveOrders` that rejects
* after ~600ms, and the `.catch()` handler calls `actions.setCellErrors` to paint the exact cell —
* the same ring/tint/aria-invalid a pre-commit `validate` rejection uses, but after the value already
* committed. Editing that cell again, with any value, clears its error on the next successful commit.
*/
export default function DataGridCellErrorsDemo(): ReactNode {
const grid = useDataGridState(initialRows(), { getRowId });
const { store, actions } = useDataGridStoreProps({
data: grid.data,
getRowId: grid.getRowId,
onUndo: grid.onUndo,
onRedo: grid.onRedo,
columns,
onDataChange: handleDataChange,
});
function handleDataChange(next: readonly Order[], change: DataChange<Order>): void {
grid.onDataChange(next, change);
saveOrders(change.ops).catch((tooMany: RejectedCell[]) => {
actions.setCellErrors(tooMany.map((cell) => ({ ...cell, message: "Quantity cannot exceed 100" })));
});
}
return (
<DataGridProvider store={store} columns={columns} getRowId={grid.getRowId}>
<div className="w-full flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
Set a Quantity above 100 and press Enter: the fake server rejects it after ~600ms and the
cell is marked with a red ring — hover it to read the message.
</p>
<div className="h-[300px] overflow-hidden rounded-md border border-border">
<DataGridRoot className="h-full rounded-none border-none">
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</div>
<CellErrorsBar />
</div>
</DataGridProvider>
);
}