Skip to content

iyulab/flex-table

Repository files navigation

flex-table

A lightweight, schema-agnostic data grid web component built with Lit.

Designed for effortless data input and crystal-clear visibility. Bridges the gap between spreadsheet freedom and database structural integrity.

Install

npm install @iyulab/flex-table

Quick Start

<flex-table id="table" row-height="32" show-row-numbers></flex-table>

<script type="module">
  import '@iyulab/flex-table';

  const table = document.getElementById('table');

  table.columns = [
    { key: 'name', header: 'Name', type: 'text', width: 200 },
    { key: 'age', header: 'Age', type: 'number', width: 100 },
    { key: 'active', header: 'Active', type: 'boolean', width: 80 },
  ];

  table.data = [
    { name: 'Alice', age: 30, active: true },
    { name: 'Bob', age: 25, active: false },
  ];
</script>

Features

  • Virtual Scroll — Smooth scrolling through 100,000+ rows (horizontal + vertical)
  • Keyboard Navigation — Arrow, Tab, Home, End, Ctrl+Home/End
  • Inline Editing — Enter/F2 to edit, Escape to cancel, type-aware editors
  • Custom Editoreditor callback for fully custom cell editing UI
  • Validationvalidator callback with visual feedback (red border + aria-invalid)
  • Range Selection — Shift+Arrow, Shift+Click for multi-cell selection
  • Column Selection — Ctrl+Click header or selectColumn() API
  • Row Selection — Checkbox-based row selection (selectable, single/multi mode)
  • Clipboard — Ctrl+C/X/V with TSV format (Excel/Google Sheets compatible, RFC 4180)
  • Sorting — Click header to sort (asc/desc/none), Shift+click for multi-sort
  • Column Resize — Drag header border, double-click to auto-fit, Alt+Arrow keyboard resize
  • Column OperationsaddColumn(), deleteColumn(), moveColumn() with undo
  • Pinned Columns — Freeze columns to left or right (pinned: 'left' | 'right')
  • Filtering — Programmatic API + built-in header filter UI (show-filters)
  • Filter Types — Text search, number range, boolean toggle, date/datetime range picker
  • Row OperationsaddRow(), deleteRows(), updateRows() with undo
  • Undo/Redo — Ctrl+Z / Ctrl+Y for all operations; configurable stack size
  • Export — CSV, TSV, JSON; full data or selection-only
  • Dark Theme — Auto via prefers-color-scheme, or manual theme="dark"
  • Row Numbers — Optional show-row-numbers attribute with sticky positioning
  • Footer Row — Summary/aggregate row via footer-data property
  • Data Mode — Client-side or server-side sorting/filtering (dataMode)
  • Context Menucontext-menu event for custom right-click menus
  • React Wrapper@iyulab/flex-table/react subpath for idiomatic React usage
  • ARIArole="grid", aria-sort, aria-selected, aria-readonly, aria-invalid, aria-rowcount, aria-colcount

Properties

Property Attribute Type Default Description
columns ColumnDefinition[] [] Column definitions
data DataRow[] [] Data rows (Record<string, unknown>[])
rowHeight row-height number 32 Row height in pixels
showRowNumbers show-row-numbers boolean false Show row number column
theme theme 'light' | 'dark' auto Force theme; auto-detects prefers-color-scheme
editable editable boolean true Global read-only mode when false
showFilters show-filters boolean false Show built-in header filter dropdowns
maxRows max-rows number 0 Max row count (0 = unlimited); blocks addRow() and paste expansion
maxUndoSize max-undo-size number 100 Max undo history stack size
selectable selectable boolean false Enable row-level checkbox selection
selectionMode selection-mode 'single' | 'multi' 'multi' Row selection mode
dataMode data-mode 'client' | 'server' 'client' Client-side or server-side data processing
footerData footer-data Record<string, string> null Footer/summary row data (keys match column keys)

Read-only Properties

Property Type Description
visibleColumns ColumnDefinition[] Columns where hidden !== true
filteredRowCount number Number of rows after filtering
canUndo boolean Whether undo is available
canRedo boolean Whether redo is available
activeCell CellPosition | null Currently focused cell { row, col }
editingCell CellPosition | null Currently editing cell { row, col }
sortCriteria SortCriteria[] Active sort criteria [{ key, direction }]
filterKeys string[] Column keys with active filters

Column Definition

interface ColumnDefinition {
  key: string;             // Unique key matching data property names
  header: string;          // Display header text
  type?: ColumnType;       // 'text' | 'number' | 'boolean' | 'date' | 'datetime'
  width?: number;          // Column width in pixels (default: 120)
  minWidth?: number;       // Minimum width in pixels (default: 40, enforced in rendering)
  hidden?: boolean;        // Hide column from view
  sortable?: boolean;      // Enable sorting (default: true)
  editable?: boolean;      // Per-column edit control (follows global editable)
  pinned?: 'left' | 'right'; // Freeze column during horizontal scroll
  renderer?: CellRenderer; // Custom cell render: (value, row, col) => TemplateResult | string
  editor?: CellEditor;     // Custom cell editor: (value, row, col) => TemplateResult
  validator?: CellValidator; // Validate before commit: (value, row, col) => string | null
}

The editor callback must return a Lit TemplateResult containing an input element with class "ft-editor". The component reads .value from that element on commit. See Custom Editor for details.

The validator callback returns null if valid, or an error message string. On failure, the cell shows a red border for 3 seconds and a validation-error event is dispatched.

Methods

Row Operations

Method Returns Description
addRow(row?, index?) DataRow | null Add a row. Returns null if maxRows reached
deleteRows(indices?) void Delete rows by data index (default: selected rows)
updateRows(changes) void Batch update cells as single undo action. changes: Array<{ row, key, value }>
refreshData() void Force re-render after in-place data mutation

Column Operations

Method Returns Description
addColumn(def, index?) ColumnDefinition Add column at position (default: end)
deleteColumn(key) void Remove column + cleanup filters/sort/widths
moveColumn(key, newIndex) void Reorder column to target index (clamped)
getColumnWidth(key) number | undefined Get internal resize width for column
selectColumn(colIndex) void Select entire column (range selection)

Row Selection

Method Returns Description
selectAll() void Select all visible rows (multi mode only)
deselectAll() void Deselect all rows
getSelectedRows() { selectedIndices, selectedRows } Get selected row data

Filtering

Method Returns Description
setFilter(key, predicate) void Set column filter. predicate: (value, row) => boolean
removeFilter(key) void Remove filter for a column
clearFilters() void Remove all filters

Export

Method Returns Description
exportToString(format, options?) string Export to 'csv' / 'tsv' / 'json'. Pass { selectionOnly: true } for selection range
exportToFile(format, filename?) void Export and trigger browser file download

Events

All events use CustomEvent with bubbles: true, composed: true.

Cell Events

Event Detail Description
cell-select { row, col } Cell focus changed
cell-edit-start { row, col, key, value } Cell editing started
cell-edit-commit { row, col, key, oldValue, newValue } Cell value committed
cell-edit-cancel { row, col } Cell edit cancelled (Escape)
validation-error { row, col, key, value, error } Cell validator rejected value

Data Events

Event Detail Description
row-add { row, index } Row added
row-delete { indices, rows } Rows deleted
batch-update { changes: [{ row, key, oldValue, newValue }] } Batch update applied

Column Events

Event Detail Description
column-add { column, index } Column added
column-delete { column, key, index } Column removed
column-reorder { key, oldIndex, newIndex } Column moved
column-resize { key, width, colIndex } Column resized (drag, auto-fit, or keyboard)
column-select { colIndex, key, rowCount } Entire column selected

Sort & Filter Events

Event Detail Description
sort-change { criteria: [{ key, direction }] } Sort criteria changed
filter-change { keys, filteredCount } Filter added/removed
filter-error { error, row, filterKey } Filter predicate threw an error

Selection Events

Event Detail Description
selection-change { selectedIndices, selectedRows } Row checkbox selection changed

Clipboard Events

Event Detail Description
clipboard-copy { range, text } Range copied as TSV
clipboard-cut { range, text } Range cut as TSV
clipboard-paste { changes, addedRows } Data pasted from clipboard
clipboard-error { action, error } Clipboard API failed (action: 'copy' or 'paste')

State Events

Event Detail Description
undo-state-change { canUndo, canRedo } Undo/redo availability changed
context-menu { x, y, row, col, dataRow, column } Right-click on cell

CSS Custom Properties

All colors and styles are customizable via CSS custom properties:

flex-table {
  --ft-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --ft-font-size: 14px;
  --ft-border-color: #e0e0e0;
  --ft-bg: #fff;
  --ft-text-color: #202124;
  --ft-header-bg: #f8f9fa;
  --ft-header-hover-bg: #e8eaed;
  --ft-header-text-color: #202124;
  --ft-row-even-bg: #fff;
  --ft-row-odd-bg: #fafafa;
  --ft-row-hover-bg: #f0f4ff;
  --ft-active-color: #1a73e8;
  --ft-selection-bg: #e8f0fe;
  --ft-bool-color: #2196f3;
  --ft-sort-indicator-color: #5f6368;
  --ft-editor-bg: #fff;
  --ft-empty-color: #999;
}

Keyboard Shortcuts

Key Action
Arrow keys Navigate cells
Tab / Shift+Tab Move to next/previous cell
Enter / F2 Start editing
Escape Cancel edit / clear selection
Home / End Row start/end
Ctrl+Home / Ctrl+End Table start/end
Shift+Arrow Extend selection range
Ctrl+C / Ctrl+X Copy/Cut selection as TSV
Ctrl+V Paste TSV data
Delete / Backspace Clear selected cells
Ctrl+Z Undo
Ctrl+Shift+Z / Ctrl+Y Redo
Alt+ArrowLeft / Alt+ArrowRight Resize current column (±20px)
Ctrl+Click header Select entire column

Usage Guide

React

Install peer dependencies and import the React wrapper:

npm install @iyulab/flex-table @lit/react react
import { FlexTableReact } from '@iyulab/flex-table/react';

function App() {
  const columns = [
    { key: 'name', header: 'Name', type: 'text' },
    { key: 'age', header: 'Age', type: 'number' },
  ];

  const data = [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 },
  ];

  return (
    <FlexTableReact
      columns={columns}
      data={data}
      showRowNumbers
      onCellEditCommit={(e) => console.log('Edited:', e.detail)}
      onSortChange={(e) => console.log('Sort:', e.detail)}
    />
  );
}

All <flex-table> properties are available as React props, and all custom events are mapped to on* callbacks (e.g., cell-edit-commitonCellEditCommit).

Custom Editor

The editor callback lets you provide a fully custom editing UI. The component reads .value from the element with class ft-editor when committing.

import { html } from 'lit';

table.columns = [
  {
    key: 'color',
    header: 'Color',
    type: 'text',
    editor: (value) => html`
      <input class="ft-editor" type="color" .value=${String(value ?? '#000000')}
        @blur=${(e) => e.target.dispatchEvent(new Event('change', { bubbles: true }))}
        @keydown=${(e) => {
          if (e.key === 'Escape') e.target.blur();
        }}>
    `,
  },
];

Key rules:

  • Must include an element with class ft-editor — the component reads its .value on commit
  • Clicking another cell auto-commits the editor
  • For Enter/Escape support, handle @keydown in your template
  • For blur-to-commit, handle @blur in your template

Validation

Use the validator callback to validate input before committing. Returns null if valid, or an error message:

table.columns = [
  {
    key: 'age',
    header: 'Age',
    type: 'number',
    validator: (value) => {
      const n = Number(value);
      if (n < 0 || n > 150) return 'Age must be 0–150';
      return null;
    },
  },
];

When validation fails, the cell displays a red border for 3 seconds and the validation-error event fires.

Pinned Columns

Freeze columns on either side during horizontal scroll:

table.columns = [
  { key: 'id', header: 'ID', pinned: 'left' },
  { key: 'name', header: 'Name' },
  // ... many columns ...
  { key: 'actions', header: 'Actions', pinned: 'right' },
];

Data Mutation

The data property uses in-place mutation for performance. Direct changes to data objects are not automatically detected:

// Will NOT trigger re-render:
table.data[0].name = 'Alice';

// Options to trigger re-render:
table.refreshData();             // Force re-render
table.updateRows([               // Recommended — includes undo support
  { row: 0, key: 'name', value: 'Alice' }
]);

Use updateRows() for programmatic edits — it provides undo/redo and dispatches the batch-update event.

Built-in Filter UI

Enable with show-filters attribute. Filter dropdowns appear in column headers:

  • text: case-insensitive substring search
  • number: min/max range inputs
  • boolean: All / True / False select
  • date: from/to date range picker (<input type="date">)
  • datetime: from/to datetime range picker (<input type="datetime-local">)

Filters set via the UI and the programmatic API (setFilter()) share the same filter state. Filter dropdowns automatically flip upward when near the viewport bottom.

Server-Side Mode

Set data-mode="server" to disable client-side sorting/filtering. The component dispatches sort-change and filter-change events but does not recompute data — your server provides pre-sorted/filtered data:

table.dataMode = 'server';
table.addEventListener('sort-change', (e) => {
  fetchData({ sort: e.detail.criteria }).then(data => {
    table.data = data;
  });
});

Development

npm install
npm run dev      # Dev server with demo
npm test         # Run tests (179 tests)
npm run build    # Build library
npm run lint     # ESLint check

License

MIT

About

Lightweight schema-agnostic data grid web component built with Lit — virtual scrolling for 100k+ rows, inline editing, Excel-compatible clipboard, and multi-column sort/filter.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors