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 point | Factory | Needs |
|---|---|---|
| modules/react | createLatticeGrid({ React, createGrid }) | Returns a component. Forwards a ref exposing .grid. |
| modules/vue | createLatticeGrid({ vue, createGrid }) | Returns a Vue 3 component definition. |
| modules/svelte | createLatticeAction({ createGrid }) | Returns a use: action. |
| Prop | Type | Does |
|---|---|---|
| any config key | as documented below | Applied through grid.setAll() when the reference changes. Never rebuilds the grid. |
| sort | SortEntry[] | grid.sort.set() |
| filters | FilterSet | grid.filters.set() |
| quickFilter | string | { text, mode } | grid.filters.quick() |
| selectedKeys | string[] | grid.selection.set() |
| on<Event> | (e: GridEvent) => void | One per event. cell:changed → onCellChanged in React; @cell-changed in Vue; on:cell-changed in Svelte. |
| className, style, id | string | object | React 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.
| File | Needed? | What it is |
|---|---|---|
| lattice-grid.min.js | yes | The whole product as a UMD build: core, renderer, editors, exports. Defines window.LatticeGrid, and also works with AMD or CommonJS loaders. |
| lattice-grid.min.css | yes | The single stylesheet. Without it the grid is in the DOM and unreadable, no column widths, no scrolling, no theme. |
| lattice-grid.esm.min.js | alternative | The same thing as an ES module, if you are importing rather than script-tagging. |
| lattice-grid.d.ts | optional | Type declarations, for editor tooling. |
| the unminified builds | optional | lattice-grid.js, .esm.js, .css: readable source for debugging. Ship the minified ones. |
| Signature | Returns | Notes |
|---|---|---|
| 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>
| Attribute | Config key | Notes |
|---|---|---|
| theme | theme | |
| density | density | |
| locale | locale | |
| row-key | rowKey | |
| row-height | rowHeight | Number. |
| header-height | headerHeight | Number. |
| auto-height | autoHeight | Boolean attribute. |
| selection | selection | none, single or multiple. |
| rows | rows | JSON. Prefer the property. |
| columns | columns | JSON. Prefer the property. |
| Property | Type | Notes |
|---|---|---|
| rows | unknown[] | Row data. A structure, so a property rather than an attribute. |
| columns | Column[] | Column definitions. |
| config | GridConfig | Merges any configuration without an attribute of its own. |
| grid | Grid | null | Read-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
| Namespace | Covers |
|---|---|
| .data | add, update, remove, removeAll, parse, load, find, findAll, exists, getItem, getId, getIndex, getLength, forEach, serialize, sort, filter, resetFilter. Index means current display order throughout. |
| .selection | setCell(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. |
| .history | undo, redo, canUndo, canRedo, clear, getHistory. |
| .export | csv, xlsx. pdf/png throw, no raster export to translate to. |
| .events | cellClick/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. |
| .rangeSelection | Best-effort only: its range shape is this wrapper's own design, not dhtmlx's genuine RangeSelection module. |
| Config key | Becomes |
|---|---|
| dragItem: 'row' | rowReorder: true: same-grid drag-to-reorder. |
| rowTransfer | Passed 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().
| Export | Does |
|---|---|
| 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
| Export | Does |
|---|---|
| createGrid, autoInit, hydrateTable, readTable, serialiseState, restoreState | The 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 history | serialiseState/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.
| Event | Payload | Fires 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:changed | The column set was replaced or reordered wholesale. | |
| columns:tagged | A column's tags changed. | |
| detail:toggled | A master-detail row opened or closed. | |
| formatting:changed | A conditional formatting rule was added, edited, reordered or restated. | |
| redaction:changed | A redaction rule changed. | |
| diff:swapped | The baseline and the current rows were exchanged. | |
| facet:computed | A header histogram finished counting. Carries the column and the buckets. | |
| facet:filtered | A bucket or a dragged range was applied as a filter. | |
| facet:expanded | The facet band was opened or collapsed. | |
| facet:failed | A distribution could not be computed. Carries the reason. | |
| form:opened | The row form opened. | |
| form:closed | The row form closed without saving. | |
| form:saved | The row form committed. | |
| form:error | A commit from the form failed validation or was rejected. | |
| tree:loading | Children are being fetched for a node. | |
| tree:loaded | Children arrived. Carries the key and the count. | |
| tree:loadFailed | A child fetch failed. | |
| tree:loadAborted | A child fetch was cancelled, usually because the node collapsed. | |
| rows:paused | A live feed was paused; updates queue from here. | |
| rows:resumed | The feed resumed and the queue drained. | |
| rows:deferred | Updates were held rather than applied, because an edit is in flight. | |
| row:received | A row arrived from a source. | |
| row:sent | A row was written back to a source. | |
| row:copied | A row was duplicated. | |
| row:moved | A row was dragged to a new position. | |
| stream:evicted | A streaming source dropped rows to stay within its cap. | |
| header:contextmenu | A heading was right-clicked. | |
| timeline:attached | A time brush was connected to the grid. | |
| timeline:detached | The brush was removed. | |
| timeline:seek | The brush settled on a range. | |
| timeline:seeking | The brush is being dragged. Throttled. | |
| presentation:changed | Presentation mode started, or its options changed. | |
| presentation:ended | Presentation mode ended. Annotations are cleared here. | |
| presentation:view | The presentation advanced to another saved view. | |
| presentation:scale | The presentation zoom changed. | |
| presentation:spotlight | A region was spotlit or released. | |
| presentation:captured | A PNG was taken. | |
| comment:added | A comment was posted. | |
| comment:edited | A comment was changed. | |
| comment:deleted | A comment was removed. | |
| comment:failed | A comment could not be saved. Carries the reason. | |
| comment:threadOpened | A thread was opened in the panel. | |
| comment:threadClosed | A thread was closed or resolved. | |
| comment:indexLoaded | The comment index finished loading, so indicators can paint. | |
| presence:published | This client's cursor or selection was broadcast. | |
| presence:left | A peer disconnected. | |
| presence:failed | A presence transport error. Presence is lossy by design; this is informational. | |
| presence:lockRefused | An 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.