bulk-editor
Spreadsheet-style bulk data editor grid — Excel / Smartsheet interactions for the web.
A keyboard- and mouse-driven grid for editing many records at once: range selection, type-to-edit, fill-down, TSV copy/paste, sort & filter, validation, per-cell locking, column resize/reorder, an aggregation footer, and optional spreadsheet formulas — all backed by a headless engine you can drive on its own.
Installation
pnpm add @fluixi/bulk-editor
Usage
import { BulkEditor } from '@fluixi/bulk-editor';
import type { BulkEditorColumn, BulkEditorRow } from '@fluixi/bulk-editor';
const columns: BulkEditorColumn[] = [
{ key: 'sku', header: 'SKU', align: 'left' },
{ key: 'price', header: 'Price', type: 'number', aggregate: 'avg' },
{ key: 'stock', header: 'Stock', type: 'number', aggregate: 'sum' },
{ key: 'active', header: 'Active', type: 'checkbox' },
];
const rows: BulkEditorRow[] = [
{ id: 'g', isGroup: true, label: 'T-Shirt' },
{ id: 'r1', label: 'Small / Black', data: { sku: 'TS-S-BLK', price: '19', stock: '120', active: '1' } },
];
<BulkEditor
columns={() => columns}
rows={() => rows}
onCellChange={(rowId, colKey, value) => save(rowId, colKey, value)}
/>
Interactions
- Select — click a cell, shift-click / shift-arrow to extend, drag to sweep a
range, click a column header or row label for the whole column/row, corner cell
or
Ctrl+Afor all. - Edit — double-click,
Enter,F2, or just start typing;Tab/Entercommit and move,Escapecancels. Committing with a range selected fills it. Number columns edit as free text, so a value or a=formulaare both accepted. - Bulk —
Deleteclears,Ctrl+D(or the fill handle) fills down,Ctrl+C/Ctrl+Vcopy and paste TSV,Ctrl+Z/Ctrl+Yundo and redo. - Columns — drag a header's right edge to resize, drag the header itself to reorder, click the caret to sort. Right-click a header or cell for its menu.
Columns & rows
| Column field | Description |
|---|---|
key |
Addresses the value in each row's data. |
header |
String or a render function. |
type |
text | number | checkbox. |
width / align |
Initial width (px) and cell alignment. |
readOnly / placeholder |
Non-editable cells, empty-cell hint. |
aggregate |
Footer fold: 'sum', 'avg', 'count', 'min', 'max', or a custom (values, ctx) => string. |
validate |
(value, ctx) => message | null — flags the cell when it returns a message. |
format |
(computed, ctx) => string — display-only transform (the raw value is still edited). |
compute |
(ctx) => string — a derived, read-only column. |
headerExtra |
Node rendered after the header label. |
disable |
Per-cell guard (ctx) => boolean. |
Rows carry data keyed by column key. isGroup rows are non-editable section
headers (with optional groupData, subLabel, thumbnail); rowActions
renders a trailing actions cell.
Props
| Prop | Description |
|---|---|
columns / rows |
Accessors for the column and row arrays. |
labelHeader / labelWidth |
Content and width of the sticky label column. |
maxHeight |
Caps the grid height; the body scrolls past it. |
filterRow |
Shows a per-column filter input row under the header. |
sortable |
Sort carets in each header (default true). |
resizable / reorderable |
Column resize and drag-reorder (default true). |
footer |
Show the aggregation footer. Defaults to auto — visible when any column has an aggregate. |
footerLabel |
Content for the footer's sticky label cell. |
contextMenu |
Enable right-click menus (default true). |
contextMenuItems / columnMenuItems |
Append custom items to the cell / column menus. |
evaluate |
Turns a raw cell value into its computed value — see Formulas. |
onCellChange |
(rowId, colKey, value) on every raw-value change. |
Sort, filter & validate
Sorting and filtering produce a flat view over the data (group rows are hidden
while active). Filter terms accept comparison and modulo operators on number
columns — >20, <=5, !=0, %3, %3=1 — and fall back to a case-insensitive
substring match otherwise. A column's validate flags invalid cells (danger
skin + aria-invalid); read them back through state.errors() / state.hasErrors().
Aggregation footer
Give a column an aggregate and a sticky footer appears, folding over the
filtered view so totals track the active filter:
{ key: 'price', header: 'Price', type: 'number', aggregate: 'avg' }
{ key: 'stock', header: 'Stock', type: 'number', aggregate: 'sum' }
{ key: 'revenue', header: 'Revenue', aggregate: (vals) => `$${vals.reduce((n, v) => n + +v, 0)}` }
Locking
Cells, rows, and columns can be locked against edits. Per-cell and per-row padlock buttons reveal on hover; the context menus offer lock/unlock for the cell, row, column, or selection. Locked cells skip every write path (edit, fill, paste, checkbox toggle).
Formulas
Every cell has a raw value (what was typed) and a computed value shown in
the grid. Pass an evaluate hook to derive it. The companion
@fluixi/formula package ships a ready evaluator — wire it with
one line:
import { BulkEditor, formulaEvaluator } from '@fluixi/bulk-editor';
<BulkEditor columns={() => columns} rows={() => rows} evaluate={formulaEvaluator()} />
Cells beginning with = become formulas — arithmetic, A1 references and ranges,
and functions like SUM, AVERAGE, IF:
=B2*C2 → price × stock for that row
=SUM(D1:D5) → total of a range
=IF(C2>0,"in stock","sold out")
References resolve against the original row order (stable under sort/filter), and
a genuine circular reference surfaces as #CYCLE!. Because a formula cell reads
other cells, changing an input recomputes every cell that depends on it — that's
by design. Pass custom functions via formulaEvaluator({ functions }).
Headless engine
The grid is a thin layer over createBulkEditorState, which owns the selection,
edit buffer, view (sort/filter), locks, validation, and bulk operations with no
DOM — usable on its own:
import { createBulkEditorState } from '@fluixi/bulk-editor';
const grid = createBulkEditorState({ columns, rows, onCellChange, evaluate });
grid.selectAll();
grid.fillDown();
grid.setColumnWidth('price', 160);
grid.moveColumn('active', 'sku');
const total = grid.getColumnAggregate('stock');
const tsv = grid.getSelectionTSV();
A1 addressing helpers (colToLabel, labelToCol, parseAddress, cellAddress)
and the filter matcher (matchesFilter) are exported too.
Part of the Fluixi UI component library. Made with ☕ by the Fluixi team.