JavaScript Data Grid
@lemonadejs/datagrid · ✓ 33 contract checks · framework-agnostic · zero dependencies
<Datagrid /> — the lightweight, virtualized data grid. Built FOR the
v6 engine’s mutable-state model: pass data by reference, mutate it
and touch() — the grid re-renders only its visible window. 100k rows
keep ~a viewport of DOM alive.
- virtual scrolling (fixed rowheight), sticky header, one scroller
- sorting (header click: asc → desc → off), numeric-aware
- search across all columns (built-in box or api.setSearch)
- selection: ‘single’ (row click) or ‘multiple’ (checkbox column with select-all)
- inline editing (double-click or Enter; Escape cancels; commits mutate YOUR row objects and fire onchange)
- keyboard: arrows move the active cell, Enter edits, Space toggles selection; ArrowUp past the first row reaches the header row, where Enter/Space toggles sort and Shift+ArrowLeft/Right resizes the column
- pagination mode (pagination > 0) instead of virtual scroll
- column customization: drag the header edge to resize (widths turn px, oncolumnresize on release), column.hidden + api.setColumn for runtime changes, headerrender for custom header content, column.class on body cells
- remote data (v5 parity):
urlfetches the rows on mount when data is empty; withremotethe SERVER owns search/sort/paging — every page change, search and sort re-fetches with ?pagination=&page=&orderBy=&asc=&term= and the response’s { result, total } feeds the window and the pager (caller-owned totals), a bare array works too - zebra striping (
zebra), themable via —lm-* tokens resizablegates the header drag handles. v5 defaulted to OFF; v6 keeps its always-on behavior as the default (true) so existing v6 users are unaffected — pass resizable=“false” for v5’s default- api.setValue(x, y, value): x is the column index OR name, y the row index (v5 argument order). v5 fired onupdate; v6 funnels it into onchange with the (row, columnName, value, oldValue) shape
- onchangepage(page) + onsearch(query, total): v5 passed the instance — v6 events are data-only
Not a spreadsheet: no formulas, no merged cells — that is jspreadsheet.
Example
import { html } from 'lemonadejs';
import Datagrid from '@lemonadejs/datagrid';
const columns = [
{ name: 'id', title: 'ID', type: 'number', width: '60px' },
{ name: 'name', title: 'Customer', width: '2fr', editable: true },
{ name: 'country', title: 'Country', width: '90px', align: 'center' },
{ name: 'amount', title: 'Amount', type: 'number', editable: true, render: (v) => '$' + Number(v).toFixed(2) },
];
const App = (props, { state }) => {
const rows = state([
{ id: 1, name: 'Ana Silva', country: 'BR', amount: 1250.5 },
{ id: 2, name: 'Bruno Mendes', country: 'PT', amount: 830 },
{ id: 3, name: 'Carla Rocha', country: 'BR', amount: 2140.75 },
{ id: 4, name: 'Daniel Keller', country: 'DE', amount: 410.2 },
{ id: 5, name: 'Eva Tanaka', country: 'JP', amount: 1975 },
{ id: 6, name: 'Felix Moreau', country: 'FR', amount: 655.9 },
{ id: 7, name: 'Gina Costa', country: 'PT', amount: 1320 },
{ id: 8, name: 'Hugo Weber', country: 'DE', amount: 98.4 },
]);
const note = state('Click a header to sort, double-click Customer or Amount to edit.');
return html`<div>
<${Datagrid} data="${rows}" columns="${columns}" height="${220}" search selectable="single"
onchange="${(row, column, value, old) => (note.value = `${row.name} — ${column}: ${old} → ${value}`)}"
onselect="${(selected) => (note.value = 'Selected: ' + selected.map((r) => r.name).join(', '))}" />
<p style="font-size:13px">${note}</p>
</div>`;
};Installation
npm install @lemonadejs/datagrid
import Datagrid from '@lemonadejs/datagrid';
import '@lemonadejs/datagrid/style.css';
Three deployment forms, one component:
html`<${Datagrid} />` // by value (no registration)
setComponents({ Datagrid }); // then <Datagrid /> by name anywhere
createWebComponent(Datagrid); // <lm-datagrid> in plain HTML/any framework
Props
Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.
| Prop | Type | Default | Description |
|---|---|---|---|
data | array | — | row objects BY REFERENCE (mutate + touch()) |
columns | array | — | Column[] |
height | number | 360 | viewport height (virtual mode) |
rowheight | number | 36 | |
selectable | string | '' | ” |
editable | boolean | false | grid default; column.editable overrides |
search | boolean | false | built-in search box |
pagination | number | 0 | rows per page; 0 = virtual scroll |
url | string | '' | fetch rows on mount when data is empty (v5: url) |
remote | boolean | false | with url: server-side search/sort/pagination (v5: remote) |
zebra | boolean | false | stripe every second row |
resizable | boolean | true | header drag-resize handles (v5 default: false) |
Events
All event names are lowercase (the platform convention — LJS-305 warns otherwise).
onchange— (row, columnName, value, oldValue)onselect— (selectedRows)onsort— (columnName, direction | null)onrowclick— (row, event)oncolumnresize— (columnName, widthPx) on handle releaseonchangepage— (page) — zero-based, after the page actually movesonsearch— (query, total) — total after the filter/fetch settles
API
import { ref } from 'lemonadejs';
const datagrid = ref();
html`<${Datagrid} ref="${datagrid}" />`;
// datagrid.current.getSelected(...) · datagrid.current.setSearch(...) · datagrid.current.sort(...) · datagrid.current.page(...) · datagrid.current.refresh(...) · datagrid.current.setColumn(...) · datagrid.current.setValue(...)
getSelected()setSearch()sort()page()refresh()setColumn()setValue()
Styling
All classes follow the lm-datagrid-* convention; visual variants are data-*
attributes on the root. Override freely — there is no styling engine to fight.
Contract
The machine-readable schema ships with the package:
import contract from '@lemonadejs/datagrid/contract.json';
verify.json carries the conformance proof produced by verify(Datagrid).
Looking for the v5 plugin? See the archived v5 documentation.