GitHub ↗

Examples

Loading preview…

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+A for all.
  • Edit — double-click, Enter, F2, or just start typing; Tab / Enter commit and move, Escape cancels. Committing with a range selected fills it. Number columns edit as free text, so a value or a =formula are both accepted.
  • BulkDelete clears, Ctrl+D (or the fill handle) fills down, Ctrl+C / Ctrl+V copy and paste TSV, Ctrl+Z / Ctrl+Y undo 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.