React quick start
import { ColumnType, Grid, type ReactColDef } from "@agility-workbench/react-grid";
const columns: ReactColDef[] = [
{ key: "name", label: "Product" },
{ key: "price", label: "Price", type: ColumnType.CURRENCY, editable: true },
];
export function Products() {
return (
<div style={{ height: 420 }}>
<Grid
rowIdKey="id"
rowData={rows}
columnDefs={columns}
onGridReady={(api) => console.log("Ready", api)}
/>
</div>
);
}
ReactColDef accepts React cell renderers, editors, tooltips, and ActionFrame
components. <Grid /> also accepts every core GridOptions field as a prop.
Both take your row type: ReactColDef<Trade>[] and <Grid<Trade> … /> type the
row in callbacks, ref, apiRef and onGridReady (see
Typing your rows).
Use ref, apiRef, or onGridReady for imperative API access. onGridReady
fires once per grid instance with the columns and row data already applied, so
API calls that address columns or rows by id resolve there. Setup and cleanup
support React Strict Mode effect replay.
React components in grid slots
function StatusBadge({ value, valueFormatted, isGroup }: CellRendererParams) {
if (isGroup) return <>{valueFormatted}</>; // a group row: its aggregate as text, or nothing
return <span className={`badge badge-${value}`}>{String(value)}</span>;
}
function OwnerTooltip({ data }: TooltipComponentParams) {
return <strong>{data.owner} · {data.ownerEmail}</strong>;
}
function CommentFrame({ value, close }: ActionFrameComponentParams) {
return <button onClick={() => { saveComment(String(value ?? "")); close(); }}>Save comment</button>;
}
const columns: ReactColDef[] = [
{ key: "status", label: "Status", cellRenderer: StatusBadge },
{ key: "owner", label: "Owner", tooltipComponent: OwnerTooltip },
{
key: "comment",
label: "Comment",
actionFrameTrigger: "click",
actionFrameComponent: CommentFrame,
},
];
React components also work in defaultColDef, groupColumnDef, and
pivotResultColumnDef for cell renderers, editors, tooltips, header tooltips,
ActionFrames, and header components.
React header components
headerComponent (the header content: label, sort icon, expander) and
headerCellComponent (the whole header cell except its resize handle) accept
React components. Each receives the core HeaderComponentParams as props and
re-renders, keeping its state, whenever the header's sort or filter state
changes. The grid unmounts it when the header is rebuilt or the grid is
destroyed.
function SortableHeader({ displayName, sort, progressSort }: HeaderComponentParams) {
return (
<button type="button" onClick={() => progressSort()}>
{displayName} {sort.direction === "asc" ? "▲" : sort.direction === "desc" ? "▼" : ""}
</button>
);
}
const columns: ReactColDef[] = [
{ key: "name", label: "Name", headerComponent: SortableHeader },
{ key: "status", label: "Status", headerCellComponent: SortableHeader },
];
The core forms still work: a class with init/getGui/refresh/destroy
passes through untouched, and a function that returns an HTMLElement (the
core function form) is mounted as that element.
React cell editor
An editor exposes its value through a ReactCellEditorHandle ref:
const UppercaseEditor = forwardRef<ReactCellEditorHandle, ICellEditorParams>(
function UppercaseEditor({ value }, ref) {
const [draft, setDraft] = useState(String(value ?? ""));
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
getValue: () => draft.toUpperCase(),
focus: () => inputRef.current?.focus(),
}), [draft]);
return (
<input
ref={inputRef}
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
);
},
);
const column: ReactColDef = {
key: "code",
label: "Code",
editable: true,
cellEditor: UppercaseEditor,
};
Live and creation-only props
Most props are live: changing one reconfigures the mounted grid in place. A
few are read only when the grid is created, and changing one of those later
has no effect. In development builds (process.env.NODE_ENV !== "production")
the grid logs one warning per prop when that happens:
[agility-workbench] <Grid> prop "rowHeight" changed after the grid was created. It is read only at creation, so the change is ignored. Remount the grid to apply it (for example, change the <Grid>'s React key).
To apply a creation-only change, remount the grid by changing its key:
<Grid key={`grid-${rowHeight}`} rowHeight={rowHeight} rowData={rows} columnDefs={columns} />
Inline objects and arrow functions that are re-created on every render with the same contents do not count as changes.
| Props | |
|---|---|
| Live | rowData, data, columnDefs, loading; rowHover, columnHover, zebraRows, getRowClass, getRowStyle, getRowPresentation, ariaLabel, ariaLabelledBy, highlightActiveCell, cellSelection, rangeSelection, fillHandle, columnSelection, headerKeyboardNavigation, showColumnButtonsOnHover, bodyContextMenu, editTrigger, readOnlyEdit, pinnedRowsEditable, rowPinningMenu, rowInsertionMenu, suppressKeyboardEdit, suppressTypeToEdit, moveAfterEdit, commitOnBlur, asyncTransactionWaitMs, loadingMessage, noRowsMessage; toolbar, quickFilter, tooltip, columnPanel, savedViews, sheets, pagination, paginationControls, rowSelection, isRowSelectable, theme, icons, headerHeight, leafHeaderHeight, parentHeaderHeight, pinnedTopRowData, pinnedBottomRowData, isRowPinned, groupRowsSticky, groupDisplayType, groupSortMode, groupRowsSelectable, groupChildCount, pivotColumnMoveMode, serverSideDataSource, serverSideAggregationSource; pivotMode, pivotColumns; treeData.keyboardNavigationMode, treeData.enableKeyboardNavigationModeSwitch |
| Live (latest callback is called) | onGridReady, on* event callbacks, onBeforeCellCommit, fillOperation, getColumnMenuItems, multiColumnMenu (except false at mount, which removes the multi-column menu for the grid's lifetime), apiRef, className, style |
| Creation-only | rowHeight, getRowId, rowIdKey, rowDataMode, rowModelType, serverSideBlockSize, overscanRowCount, minResizeWidth, maxColumnWidth, allowExportAsCSV, allowExportAsExcel, rowNumbers, selectAllRowsOnHeaderClick, selectAllScope, selectionPersistence, clearSelectionOnBodyClick, resetPageOn, pageSize, pageSizes, paginationUnknownTotalTooltip, getGroupChildCount, autosizeColumnsOnDataChange, undoLimit, initialSort, showSortPriority, reevaluateOnEdit, groupColumnDef, groupDefaultExpanded, pivotResultColumnDef, maxPivotColumns, treeData (all other members), isFullWidthRow, fullWidthCellRenderer, defaultColDef, pivotNoValuesMessage, pivotEmptyMessage, filterDebounceMs, cellFlashDuration, cellFadeDuration, suppressStyleInjection, styleNonce |
The creation-only list is the core's CREATION_ONLY_OPTION_KEYS (every
option outside UPDATABLE_OPTION_KEYS and the pivotMode/pivotColumns
model state), minus the callbacks the binding bridges. The
Angular binding splits its
inputs the same way.
Event callback props
<Grid
onCellClicked={(event) => console.log(event.rowId, event.colId)}
onRowClicked={(event) => console.log(event.rowId)}
onCellDoubleClicked={(event) => console.log(event.rowId, event.colId)}
onRowDoubleClicked={(event) => console.log(event.rowId)}
onCellValueChanged={(event) => saveChange(event)}
onSelectionChanged={(event) => console.log(event.snapshot)}
onSortChanged={(event) => console.log(event.changedColIds)}
onFilterChanged={(event) => console.log(event.source, event.changedColIds)}
onColumnsChanged={(event) => {
if (event.source === "user") saveLayout(apiRef.current?.getColumnState());
}}
onPaginationChanged={(event) => setPage(event.pageIndex)}
onError={(event) => showToast(event.message)}
/>
onColumnsChanged reports every column change with source: "user" for gestures made
through the grid's UI and "api" for API calls, restores and columnDefs updates — persist
on "user" and restoring a saved layout never echoes back into storage (see
Telling user changes from your own).
onError is an extra notification for failed row-model requests; the grid handles the failure
as it would without it.
Every core event has a prop — onRowGroupOpened, onFirstDataRendered, onRowDataUpdated,
onBodyScroll, onFocusChanged and the rest of the
callbacks table — and each is bridged the same way:
passing a new function on a re-render is live, without recreating the grid.
api.on(eventName, handler) still works for any event.
React-aware menu items and icons
<Grid
icons={{ export: "/icons/download.svg" }}
getColumnMenuItems={({ items }) => [
...items,
{ isSeparator: true },
{ id: "help", label: "Help", left: <HelpIcon />, onClick: openHelp },
]}
bodyContextMenu={({ items }) => [
...items,
{ id: "inspect", label: "Inspect", right: <kbd>I</kbd>, onClick: inspect },
]}
/>
React nodes are accepted in menu left and right slots. The getters also receive
ctx; for the body menu, ctx.isGroup, ctx.isFullWidth and ctx.groupDisplayType
tell a right-clicked group row from a leaf row.