Lattice Grid Buy a licence

api reference

Adapters, the web component, htmx and events

The framework adapters, the web component, declarative init, the dhtmlx compatibility wrapper, htmx integration, and every event the grid emits.

API reference › Adapters, the web component, htmx and events

Framework adapters

The web component carries the grid inside it, so use it or createGrid in a page, not both: two copies keep separate registries, and a renderer registered through one is invisible to the other. One optional bundle per framework. The framework and createGrid are passed in rather than imported, so the adapters add no dependency and carry no second copy of the grid.

// React
import React from 'react';
import { createGrid } from './dist/lattice-grid.esm.js';
import { createLatticeGrid } from './dist/modules/react.esm.js';

const LatticeGrid = createLatticeGrid({ React, createGrid });
<LatticeGrid columns={columns} rows={rows} rowKey="id" onCellChanged={fn} />

// Vue 3
import * as vue from 'vue';
import { createLatticeGrid } from './dist/modules/vue.esm.js';

const LatticeGrid = createLatticeGrid({ vue, createGrid });
<LatticeGrid :columns="columns" :rows="rows" row-key="id" @cell-changed="fn" />

// Svelte, an action, so no framework runtime is needed
import { createLatticeAction } from './dist/modules/svelte.esm.js';

const lattice = createLatticeAction({ createGrid });
<div use:lattice={{ columns, rows, rowKey: 'id' }} on:cell-changed={fn}></div>
Entry pointFactoryNeeds
modules/reactcreateLatticeGrid({ React, createGrid })Returns a component. Forwards a ref exposing .grid.
modules/vuecreateLatticeGrid({ vue, createGrid })Returns a Vue 3 component definition.
modules/sveltecreateLatticeAction({ createGrid })Returns a use: action.
PropTypeDoes
any config keyas documented belowApplied through grid.setAll() when the reference changes. Never rebuilds the grid.
sortSortEntry[]grid.sort.set()
filtersFilterSetgrid.filters.set()
quickFilterstring | { text, mode }grid.filters.quick()
selectedKeysstring[]grid.selection.set()
on<Event>(e: GridEvent) => voidOne per event. cell:changedonCellChanged in React; @cell-changed in Vue; on:cell-changed in Svelte.
className, style, idstring | objectReact only. Applied to the host element, not the grid.

Published as @toclocoinc/lattice-grid: npm install @toclocoinc/lattice-grid, then import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react' (swap the module name for Vue or Svelte) resolves like any other package. Type declarations resolve automatically through the package's own types field, no @types package to install. Importing the built file by path, or the script tag, both still work for a project with no npm install step at all.

FileNeeded?What it is
lattice-grid.min.jsyesThe whole product as a UMD build: core, renderer, editors, exports. Defines window.LatticeGrid, and also works with AMD or CommonJS loaders.
lattice-grid.min.cssyesThe single stylesheet. Without it the grid is in the DOM and unreadable, no column widths, no scrolling, no theme.
lattice-grid.esm.min.jsalternativeThe same thing as an ES module, if you are importing rather than script-tagging.
lattice-grid.d.tsoptionalType declarations, for editor tooling.
the unminified buildsoptionallattice-grid.js, .esm.js, .css: readable source for debugging. Ship the minified ones.
SignatureReturnsNotes
createGrid(element, config?) Grid Resolves the document from element.ownerDocument, so a grid inside an iframe uses that frame's document. Throws with a clear message if there is no DOM.
createHeadlessGrid(config?) Grid Core only. Everything below except grid.element and the DOM-only config keys works unchanged.

<lattice-grid> web component

A self-contained module bundle that registers a custom element on import. One script, one tag, no build step, for Rails, Django, Laravel or any page without a bundler. Load this or lattice-grid.esm.js, not both: the module carries the grid with it.

<link rel="stylesheet" href="dist/lattice-grid.min.css">
<script type="module" src="dist/modules/webcomponent.esm.min.js"></script>

<lattice-grid row-key="id"
  columns='[{"field":"id"},{"field":"city"}]'
  rows='[{"id":"A","city":"Leeds"}]'></lattice-grid>
AttributeConfig keyNotes
themetheme
densitydensity
localelocale
row-keyrowKey
row-heightrowHeightNumber.
header-heightheaderHeightNumber.
auto-heightautoHeightBoolean attribute.
selectionselectionnone, single or multiple.
rowsrowsJSON. Prefer the property.
columnscolumnsJSON. Prefer the property.
PropertyTypeNotes
rowsunknown[]Row data. A structure, so a property rather than an attribute.
columnsColumn[]Column definitions.
configGridConfigMerges any configuration without an attribute of its own.
gridGrid | nullRead-only. The underlying grid, for the full imperative API.

Events are re-dispatched as CustomEvents named lattice- plus the grid name with colons hyphenated: cell:changed becomes lattice-cell-changed. The payload is on event.detail. The prefix avoids colliding with platform events, the grid emits one called scroll.

dhtmlx Grid compatibility wrapper

A Grid class shaped like dhtmlx's own dhx.Grid (Suite 5+), backed by a real Lattice grid. Covers column definitions, .data, .selection, .history, .export.csv/.xlsx, and a name-mapped subset of .events: see the guide for exactly what is and is not covered. Not the classic pre-Suite-5 dhtmlXGridObject.

import { Grid } from './dist/modules/dhtmlx-compat.esm.min.js';

const grid = new Grid(container, {
  columns: [{ id: 'name', header: [{ text: 'Name' }], sortable: true }],
  data: rows,
});
grid.data.serialize(); // every row's data, in source order
NamespaceCovers
.dataadd, update, remove, removeAll, parse, load, find, findAll, exists, getItem, getId, getIndex, getLength, forEach, serialize, sort, filter, resetFilter. Index means current display order throughout.
.selectionsetCell(rowId, colId, ctrlUp?, shiftUp?), getCell, getCells, isSelectedCell, removeCell. row/column carry .id; a row's own fields sit alongside it, matching dhtmlx's own IRow.
.historyundo, redo, canUndo, canRedo, clear, getHistory.
.exportcsv, xlsx. pdf/png throw, no raster export to translate to.
.eventscellClick/cellDblClick/cellRightClick/afterEditStart/afterEditEnd/afterSort call your handler with dhtmlx's own positional arguments. afterRowDrop fires (data, event) from a same-grid reorder settling or a row landing from another grid. Every other mapped name passes Lattice's own event object. Every before*/can*/cancel* event, and row/column drag negotiation (a handler refusing or steering a drop mid-gesture), is unmapped: logged once per name if subscribed to.
.rangeSelectionBest-effort only: its range shape is this wrapper's own design, not dhtmlx's genuine RangeSelection module.
Config keyBecomes
dragItem: 'row'rowReorder: true: same-grid drag-to-reorder.
rowTransferPassed straight through, unlike every other key: dhtmlx allows any two dragItem: 'row' grids on a page to exchange rows by default; Lattice's rowTransfer is deliberately opt-in per pair, so there is nothing to derive it from.

Declarative init, hydration & state

Core-level primitives, usable with or without any framework adapter or the htmx module below. An element built by createGrid is discoverable from itself: element.__lattice holds the live instance, cleared on destroy().

ExportDoes
autoInit(root)Builds a grid on every [data-lattice-grid] element under root not already built: idempotent, safe to call again after new content arrives. Config comes from a sibling <script type="application/json" data-lattice-config>; without one, a <table> element is hydrated instead.
hydrateTable(table, config?)Reads a <table>'s header row for column definitions and body rows for data (type-inferred per cell), then replaces the table with the grid. config (columns, rows, anything else) always wins over what was inferred.
serialiseState(grid) / restoreState(grid, encoded)A compact, URL-safe encoding of everything grid.state covers (sort, filters, column order and widths, scroll position, selection) diffed against the grid's own defaults first, so an untouched grid encodes to a handful of characters.
<meta name="lattice-license" content="…">Read automatically when no licence is passed to createGrid, no imperative call required.

htmx integration

One file. modules/htmx re-exports createGrid, autoInit, hydrateTable, readTable, serialiseState and restoreState alongside its own exports below, so a page using htmx integration never also loads the base bundle: that would mean two independent copies of the whole engine on one page. Registers on import: builds grids from [data-lattice-grid] elements or server-rendered <table>s, tears them down before htmx detaches a swapped-out subtree, and rebuilds them in newly-loaded content. driveServerMode/driveInfiniteScroll drive sort, filter and infinite scroll over plain htmx requests; driveOobUpdates applies out-of-band row updates in place. See the guide for the full request-lifecycle wiring and why infinite scroll uses two triggers.

import { autoInit, driveServerMode, driveInfiniteScroll } from './dist/modules/htmx.esm.min.js';

autoInit(document); // builds every [data-lattice-grid] under it
ExportDoes
createGrid, autoInit, hydrateTable, readTable, serialiseState, restoreStateThe same functions documented above, re-exported: this is the one import a page using htmx integration needs for both grid construction and htmx wiring.
driveServerMode(grid, trigger, opts?)A sort or filter change fires a request on trigger carrying offset/limit/sort/filters; the response replaces the grid's rows. opts.columns for the HTML-fragment ingest path.
driveInfiniteScroll(grid, sentinel, opts?)Appends rows as the grid's own visible window nears the end of what's loaded. sentinel's own hx-trigger names revealed, lattice:scroll-near-end, the first fires the initial chunk, the second every chunk after. opts.threshold (default 20) sets how many rows from the end counts as near.
driveOobUpdates(grid, opts?)Applies an out-of-band swap landing on [data-lattice-row="<key>"] to that row, in place: scroll, selection and filter state untouched.
Browser historyserialiseState/restoreState above, wired automatically to htmx:beforeHistorySave/htmx:historyRestore once this module is imported: browser back restores the prior sort, filter and scroll position.

Events

One bus. There are no onX configuration properties. Every payload also carries type, origin and grid.

const off = grid.on('cell:changed', e => save(e.row.data));
grid.once('ready', init);
grid.on('*', e => console.log(e.type, e));   // wildcard, for debugging
off();                                       // on() returns its own unsubscribe

origin is 'api', 'user' or 'init'. A host persisting state reads it to ignore its own writes and avoid a feedback loop.

EventPayloadFires when
ready{}First layout is complete and the API is safe to drive.
render:first{}First paint, the number to measure time-to-first-row against.
destroy{}grid.destroy() has run.
model:changed{ reason }Columns, grouping, pivot or another structural change.
rows:changed{ identified?, companion?, added, updated, removed, plan }The row set changed. See what a change firing promises: identified means the three arrays name the rows that moved, companion marks a duplicate announcement of a change already made with identity, and a firing with neither is a real change of unknown extent.
rows:queued{ pending }A batched change is waiting for the next frame.
cell:changed{ row, key, colId, value, oldValue, undo }A committed edit reached the data. undo distinguishes a rollback.
cell:pending{ row, key, colId, value, before, id }Applied optimistically, not yet durable. Only with edit.commit.
cell:confirmed{ row, key, colId, value, id, superseded }The write reached the server.
cell:reverted{ row, key, colId, rejected, restored, reason, id, superseded, applied }The write failed. applied: false means a newer edit owned the cell, so nothing was written back.
cell:edit:start{ row, key, colId, column }An edit session opened.
cell:edit:end{ row, key, colId, valid, errors }It closed: committed or cancelled.
cell:clicked{ row, key, index, colId, column, value, text, event }A cell was clicked. Announcement only: nothing is consumed, so editing and selection behave unchanged.
cell:dblclicked{ ...as cell:clicked }
row:clicked{ row, key, index, event }Emitted alongside the cell event, cell first.
row:dblclicked{ row, key, index, event }
row:edit:start{ row, key, colId, column }Replaces the cell pair when edit.mode is 'row'.
row:edit:end{ row, key, colId, valid, errors }In row mode an invalid cell blocks the whole commit and the session stays open.
cell:contextmenu{ ...cellParams, row }Right-click on a cell.
sort:changed{ sort }The full sort entry list.
filter:changed{ filters } | { quick }The condition tree or the quick filter changed.
group:toggled{ expanded, all? }A group row opened or closed.
column:moved{ colId, to, origin }Reordered by drag or by API.
column:resized{ colId, width, origin }
column:visible{ ids, hidden }Columns shown or hidden.
column:pinned{ id, side }side is 'start', 'end' or null.
column:grouped{ columns }The row-group column list changed.
column:pivoted{ columns } | { pivotFields, remote }
column:menu:open{ colId }Header menu opened.
column:filter:open{ colId }Header filter popup opened.
selection:changed{ keys, rows }
range:changed{ ranges }Cell range selection changed.
page:changed{ page, pageSize, total, pageCount }Fired after the rows have moved, whether the page changed by API or by the pager control.
config:changed{ key, value, oldValue } | { keys, values, oldValues }A configuration key changed. Emitted after the grid has rebuilt, so a listener reading the grid back sees the change rather than what it replaced.
scroll{ top, left }Throttled to the frame.
scroll:end{}Scrolling settled, the moment to trigger deferred work.
size:changed{}The viewport resized.
state:changed{ state, report }report lists anything a restore could not apply.
stream:chunk{ loaded, estimated, count, renders }A streamed chunk landed.
stream:end{ loaded, promoted, threshold }Streaming finished; promoted means it switched to in-memory.
source:error{ error, block?, range? }A source or block load failed.
clipboard:copy{ text, ok, rows }A copy left the grid.
export:progress{ ... }Progress on a streamed export.
toolpanel:focus{}The documented keyboard shortcut reached the tool panel.
history:changed{ canUndo, canRedo, undo, redo }Emitted after the entry is pushed, so a toolbar reading it names the right action. Repainting from sort:changed instead reads the timeline one action behind.
history:applied{ direction, step }An action was undone or redone. Distinct from history:changed, which also fires when a new action is pushed onto the stacks and so cannot tell you anything was reversed.
state:reset{ state }The grid was returned to its baseline.
highlight:changed{ highlights }A highlight was added or cleared.
redaction:changed{ columns }A column was redacted or restored.
header:contextmenu{ colId, column, element, x, y }A column heading was right-clicked.
render:done{ first, last }The cells are written and stable. Anything decorating them from outside must run after this, the cell layer rewrites each cell's className wholesale and would otherwise erase it.
views:changed{ views, reason, view, activeId? }The whole list, plus what moved and why.
view:saved{ view, views, reason }Carries the one view that moved: enough to POST a single record without diffing two lists.
view:renamed{ view, views, reason }
view:removed{ view, views, reason }
view:default{ view, views, reason }view is null when the default was cleared.
view:applied{ view, views, activeId }Emits no storage write: applying a view changes nothing to persist.
permissions:changed{ levels }The context moved and every column re-resolved.
diff:changed{ summary }A snapshot was set or cleared.
licence:changed{ info, state }A key was installed, and again when verification settles.
columns:changedThe column set was replaced or reordered wholesale.
columns:taggedA column's tags changed.
detail:toggledA master-detail row opened or closed.
formatting:changedA conditional formatting rule was added, edited, reordered or restated.
redaction:changedA redaction rule changed.
diff:swappedThe baseline and the current rows were exchanged.
facet:computedA header histogram finished counting. Carries the column and the buckets.
facet:filteredA bucket or a dragged range was applied as a filter.
facet:expandedThe facet band was opened or collapsed.
facet:failedA distribution could not be computed. Carries the reason.
form:openedThe row form opened.
form:closedThe row form closed without saving.
form:savedThe row form committed.
form:errorA commit from the form failed validation or was rejected.
tree:loadingChildren are being fetched for a node.
tree:loadedChildren arrived. Carries the key and the count.
tree:loadFailedA child fetch failed.
tree:loadAbortedA child fetch was cancelled, usually because the node collapsed.
rows:pausedA live feed was paused; updates queue from here.
rows:resumedThe feed resumed and the queue drained.
rows:deferredUpdates were held rather than applied, because an edit is in flight.
row:receivedA row arrived from a source.
row:sentA row was written back to a source.
row:copiedA row was duplicated.
row:movedA row was dragged to a new position.
stream:evictedA streaming source dropped rows to stay within its cap.
header:contextmenuA heading was right-clicked.
timeline:attachedA time brush was connected to the grid.
timeline:detachedThe brush was removed.
timeline:seekThe brush settled on a range.
timeline:seekingThe brush is being dragged. Throttled.
presentation:changedPresentation mode started, or its options changed.
presentation:endedPresentation mode ended. Annotations are cleared here.
presentation:viewThe presentation advanced to another saved view.
presentation:scaleThe presentation zoom changed.
presentation:spotlightA region was spotlit or released.
presentation:capturedA PNG was taken.
comment:addedA comment was posted.
comment:editedA comment was changed.
comment:deletedA comment was removed.
comment:failedA comment could not be saved. Carries the reason.
comment:threadOpenedA thread was opened in the panel.
comment:threadClosedA thread was closed or resolved.
comment:indexLoadedThe comment index finished loading, so indicators can paint.
presence:publishedThis client's cursor or selection was broadcast.
presence:leftA peer disconnected.
presence:failedA presence transport error. Presence is lossy by design; this is informational.
presence:lockRefusedAn edit was refused because a peer holds the cell.

This list is complete, and stays complete: tools/check.js compares every emit() in the grid against the declared event names and fails the build on a mismatch. A chart raises its own events, which belong to the charts module rather than to this bus.