api reference
Adapters API Reference
A component for every view in React, Vue, Svelte and Angular, a custom element per view, declarative init, the grid-API compatibility wrapper and htmx.
API reference › Framework adapters and custom elements
All 29 pages Everything on one page → Developer guide →
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. Since 1.63 the same entry point also builds a component for every other viewer and the data router. |
| @toclocoinc/ | <lattice-grid [config]="…"> | A compiled entry point of this package, not a bundle: standalone components compiled ahead of time, one per viewer, plus the data router as a service. No second install. See Angular. modules/angular, which needs the JIT compiler, is deprecated. |
| modules/vue | createLatticeGrid({ vue, createGrid }) | Returns a Vue 3 component definition. Since 1.66 the same entry point also builds a component for every other viewer and the data router. |
| modules/svelte | createLatticeAction({ createGrid }) | Returns a use: action. Since 1.66 the package also ships a Svelte 5 component per viewer as .svelte source at @toclocoinc/lattice-grid/svelte/*.svelte, and this entry point carries the helpers they are built from. |
| 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. |
React: every viewer, not only the grid
Until 1.63 the React adapter wrapped createGrid and nothing else. Every other
shipped viewer - the KPI panel, a chart, the board, the Gantt, the layout, the tab
strip - and the data router had no React surface, so a React application wrote its own
useEffect per viewer. It does not have to any more: there is one component per
viewer, and each keeps the contract the grid component already kept.
import React from 'react';
import ReactDOM from 'react-dom';
import { createGrid } from '@toclocoinc/lattice-grid';
import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { createLatticeReact } from '@toclocoinc/lattice-grid/modules/react';
// Built once, at module scope. Building components inside a component hands React a
// new type on every render, and a new type is a different element: the subtree would
// unmount and remount, destroying and rebuilding the grid on every keystroke.
const L = createLatticeReact({
React, ReactDOM, createGrid, createKPI, createChart, createDataRouter,
});
function Dashboard({ rows }) {
const router = L.useLatticeRouter(ROUTER_CONFIG);
return (
<L.LatticeRouterProvider router={router}>
<L.LatticeGridProvider>
<L.LatticeGrid name="quakes" route="all" {...GRID_CONFIG} rows={rows} />
<L.LatticeKPI gridName="quakes" tiles={TILES} columns={5} />
<L.LatticeChart gridName="quakes" type="bar" x="region" y="count" />
</L.LatticeGridProvider>
</L.LatticeRouterProvider>
);
}
| Export | Signature | What it is |
|---|---|---|
| createLatticeGrid | ({ React, createGrid }) | The grid component. Extended in 1.63 with rowUpdates, predicates, onGridReady, onGridDestroy, name and route. |
| createLatticeKPI | ({ React, createKPI }) | The KPI panel. Grid-bound through context by default; pass rows instead for a panel with no grid. |
| createLatticeChart | ({ React, createChart }) | A chart. Requires a grid, so nothing is mounted until one exists; a changed spec key goes to chart.update() and the chart redraws rather than being rebuilt. |
| createLatticeKanban | ({ React, createKanban }) | The board. rows, quickFilter, sprint, epic, loading and error are live props. |
| createLatticeCalendar | ({ React, createCalendar }) | The calendar. rows, view and date are live props; grid-bound through context by default. The reschedule events arrive as onBeforeMove (cancel with preventDefault(reason?)) and onCardMove; call move(key, to) on the ref's instance. |
| createLatticeGantt | ({ React, createGantt }) | The Gantt. tasks and dependencies are live props. |
| createLatticeLayout | ({ React, createLayout }) | The layout. Windows are driven through the ref; its events arrive as onLayoutChanged, onWindowMoved and the rest. |
| createLatticeTabs | ({ React, ReactDOM, createTabs, createGrid? }) | The tab strip, with React-rendered tab content: a tab's content is a React element (or a function returning one) rendered into the strip's own panel through createPortal, so a tab's grid is a real <LatticeGrid> with props, a ref and the surrounding context. A tab with no content is left to the module, so the two kinds mix on one strip. |
| createLatticeDesigner | ({ React, createDesigner }) | The designer. mode, state, sources and guardrails are live; relationships, factories and llm are mount-time - change the component's key to apply one. state is controlled when the host passes it: a new state (by identity) is pushed in with setState, and designer changes arrive through onStateChange as a copy, never as a write to the prop. The ref is the live Designer handle. |
| createLatticeGridContext | ({ React }) | Returns { LatticeGridProvider, useLatticeGrid }. A grid-bound viewer needs the grid instance, which appears after the first render; a ref cannot help because writing to a ref re-renders nobody. Several grids may publish under one provider - name on the grid, gridName on the viewer. |
| createLatticeRouter | ({ React, createDataRouter }) | Returns { useLatticeRouter, LatticeRouterProvider, useRouter }. The hook creates the router in an effect and destroys it in that effect's cleanup, so it returns null on the first render; the config is read once, because rebuilding would drop every attached grid and every row held. |
| createLatticeViewer | ({ React, viewer, mount, … }) | The generic behind all of the above, and the escape hatch for a viewer with no named factory yet. |
| createLatticeReact | ({ React, ReactDOM?, …factories }) | Every binding from one call: pass the factories the application uses and it builds those components, leaving the rest undefined. Nothing is imported, so an application that never uses the board never loads the board. |
| createViewerController | ({ viewer, mount, element, props }) | The framework-free viewer lifecycle - mount once, push changed props into the live instance, destroy - shared by every adapter. |
| VIEWER_EVENTS | Record<string, readonly string[]> | Every event each non-grid viewer emits, keyed by viewer name. |
| VIEWER_APPLY | Record<string, Record<string, Function>> | Which props each viewer can take live, and the instance call each becomes. Anything not listed is mount-time configuration. |
| viewerHandlerName | (event) => string | A viewer event as its React prop: card:move becomes onCardMove. |
| createDesignerController | ({ createDesigner, element, props }) | The framework-free designer lifecycle - mount once, push changed props into the live instance, destroy - shared by every adapter, with the controlled-state identity guard. |
| DESIGNER_EVENTS | readonly string[] | Every event the designer emits: state, mode, page, select. |
| designerHandlerName | (event) => string | A designer event as its callback prop: state becomes onStateChange, the rest onMode, onPage, onSelect. |
| DEFAULT_GRID_NAME | string | The name a grid publishes itself under when you do not choose one: default. |
const A = await import('../packages/modules/react/index.js');
// A stand-in for React and react-dom. The adapter never imports either, so a
// factory only needs the handful of names it destructures - which is exactly
// why this example runs in Node with no framework installed.
const React = { createElement: () => ({}), createContext: () => ({}), forwardRef: (f) => f };
const ReactDOM = { createPortal: () => ({}) };
const stub = () => ({ on: () => () => {}, destroy() {} });
// One call builds whichever components the factories you pass support.
const L = A.createLatticeReact({
React, ReactDOM, createGrid: stub, createKPI: stub, createChart: stub, createDataRouter: stub,
});
// …or one factory at a time.
const components = [
A.createLatticeGrid({ React, createGrid: stub }),
A.createLatticeKPI({ React, createKPI: stub }),
A.createLatticeChart({ React, createChart: stub }),
A.createLatticeKanban({ React, createKanban: stub }),
A.createLatticeCalendar({ React, createCalendar: stub }),
A.createLatticeGantt({ React, createGantt: stub }),
A.createLatticeScheduler({ React, createScheduler: stub }),
A.createLatticeLayout({ React, createLayout: stub }),
A.createLatticeTabs({ React, ReactDOM, createTabs: stub }),
A.createLatticeViewer({ React, viewer: 'kpi', mount: stub }),
A.createLatticeShell({ React, createShell: stub }),
A.createLatticeForm({ React, createForm: stub }),
].filter((c) => typeof c === 'function');
// The provider/hook pair and the router hook are built the same way.
const { LatticeGridProvider, useLatticeGrid } = A.createLatticeGridContext({ React });
const { useLatticeRouter } = A.createLatticeRouter({ React, createDataRouter: stub });
// And the lifecycle underneath, driven with no framework at all: mount once,
// push a changed live prop into the instance that already exists, destroy.
const seen = [];
const controller = A.createViewerController({
viewer: 'kpi',
element: {},
props: { rows: [{ id: 1 }] },
mount: () => ({ setRows: (r) => seen.push(r.length), on: () => () => {}, destroy() {} }),
});
controller.update({ rows: [{ id: 1 }, { id: 2 }] });
controller.destroy();
return `${components.length} components, ${A.viewerHandlerName('card:move')}, `
+ `${A.DEFAULT_GRID_NAME}, rows ${seen.join('/')}, `
+ `events ${A.VIEWER_EVENTS.kpi.length}/${Object.keys(A.VIEWER_APPLY).length}`;
The designer is not a viewer. It has no grid, so instead of
createLatticeViewer each framework wraps it directly - createLatticeDesigner({ React, createDesigner }) in React, the same in Vue,
and bindDesigner under Designer.svelte in Svelte. One
framework-free createDesignerController owns the lifecycle every adapter drives:
mount once, push changed props into the live instance, destroy. Its state prop is
controlled when the host passes it - a new state by identity is applied, and a
designer change comes back out as a copy through onStateChange, so echoing it
never loops.
const A = await import('../packages/modules/react/index.js');
const S = await import('../packages/modules/svelte/index.js');
const D = await import('../packages/modules/shared/designer.js');
// A stand-in for React; the adapter never imports it.
const React = { createElement: () => ({}), forwardRef: (f) => f, useRef: (v) => ({ current: v }), useEffect: () => {}, useImperativeHandle: () => {} };
const calls = [];
const designer = () => ({
on: () => () => {}, setState: () => calls.push('state'), setMode: () => calls.push('mode'),
setSources: () => calls.push('sources'), setGuardrails: () => calls.push('guardrails'),
destroy() { calls.push('destroy'); },
});
// The one framework-free controller: mount, apply a new state, destroy.
const c = D.createDesignerController({ createDesigner: designer, element: {}, props: { state: { pages: [] } } });
c.update({ state: { pages: [{ id: 'a' }] } });
c.destroy();
// React's (and Vue's) component, and Svelte's binding helper, both build over
// the injected factory rather than importing the designer module.
const LatticeDesigner = A.createLatticeDesigner({ React, createDesigner: designer });
const binding = S.bindDesigner({ createDesigner: designer, element: {} });
binding.sync({ state: { pages: [] } });
binding.destroy();
return `${D.DESIGNER_EVENTS.join(',')}, ${D.designerHandlerName('state')}, ${LatticeDesigner.displayName}, ${calls.join(' ')}`;
Live props versus mount-time props. The grid takes any changed configuration
key through one call (grid.setAll); no other viewer does. So each viewer declares
which props it can take while it is running - the table above, and
VIEWER_APPLY at runtime - and everything else is mount-time configuration.
A mount-time prop that changes is not silently ignored and not silently
remounted (that would throw away scroll position, selection and expansion): it is named once
in a warning that tells you to give the component a key that changes when the
rebuild is wanted, or to drive the instance through the ref.
Two props rebuild a viewer rather than update it: the grid it is bound to, and anything it cannot exist without. A chart holding a destroyed grid is not stale, it is invalid, so a new grid tears the old chart down and builds a new one against it - in that order, never the reverse.
A live feed: rowUpdates and predicates.
rowUpdates is a keyed diff handed straight to grid.rows.apply(),
applied when the object's identity changes - a feed produces a new change object per
batch, so identity is the right trigger and re-applying the same object would re-land rows the
grid already has. predicates is { name: fn } mapped to
grid.filters.where(name, fn), diffed by name, with a name that has gone removed.
Those compose with whatever filter the reader set in the tool panel; the
filters prop cannot, because it maps to filters.set and replaces the
whole condition tree.
StrictMode creates one instance. React 18's StrictMode deliberately mounts, unmounts and mounts again in development. Every component here builds its instance in an effect with an empty dependency list and destroys it in that effect's cleanup, so the first is destroyed before the second is built and exactly one survives. There is no module-level "already mounted" flag, because that would defeat a genuine remount.
Props are diffed with Object.is, and that is a promise about identity.
An inline columns={[{ field: 'a' }]} is a new array on every render, so it counts
as changed every render and reconfigures the grid every render. Hoist it to module scope or
wrap it in useMemo. This is not a defect to work around: a deep compare of a
million-row array on every render would cost more than the reload it prevents.
Sharing one rows array between grids is safe. Since 1.63 the
grid copies the array it is handed on ingest, so two grids given the same
rows={EMPTY} default no longer contaminate each other. The row objects
are still shared, as they always were - mutate one and both grids see it.
React 18 and 19, no SSR. Nothing in the adapter uses an API added in 19 or
removed in 19 (forwardRef is still supported there). Every component owns a real
DOM element, so there is no server rendering and no React Server Component support: render
them on the client.
One build warning is gone. The version resolver carried a Node-only fallback
whose import('node:' + 'module') Vite could not analyse statically, so every Vite
build of every application printed "The above dynamic import cannot be analyzed by Vite" about
a line that could never run. The build now deletes that branch from every emitted artefact.
React: <LatticeDataGrid>, a DataGrid page by one import
createLatticeDataGrid (1.80) builds a component that takes the props of the
<DataGrid> - the free-tier ones in the table below - so a page written
against a data-grid package keeps its column definitions, its controlled models and
its callbacks, and changes the import. Every accepted prop maps to documented grid configuration,
a grid API call or a subscription to a grid event. Every other prop is refused once, by
name, with the catalogued warning react:mui-prop-unsupported:<prop>, and
ignored - never half-applied. It is React-only by design: it exists for pages
leaving a data-grid package, which Vue, Svelte and Angular hosts are not, so the rule that every framework gets
every viewer does not extend to it. sx and the .MuiDataGrid-* class names are
not emulated; theme the grid through its --lattice-* tokens. The grid's stylesheet still
needs loading.
import React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createGrid } from '@toclocoinc/lattice-grid';
import { createLatticeDataGrid } from '@toclocoinc/lattice-grid/modules/react';
// Built once, at module scope. ReactDOM is needed only by a renderCell that returns an element.
export const DataGrid = createLatticeDataGrid({ React, ReactDOM, createGrid });
// …and in the page, instead of `import { DataGrid } from '@mui/x-data-grid'`:
<DataGrid rows={rows} columns={columns} checkboxSelection
sortModel={sortModel} onSortModelChange={setSortModel}
paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} />
| Prop | What it becomes |
|---|---|
| rows | the rows of a memory source (copied on ingest) |
| columns | columns, each definition translated (table below) |
| getRowId | rowKey as a function, read at call time; id by default |
| loading | grid.overlay.show('loading') / hide() |
| density | density (the three MUI values are grid values too) |
| checkboxSelection | selection: { mode: 'multiple', checkbox: true, headerCheckbox: true }; without it, mode: 'single' |
| disableRowSelectionOnClick | selection.checkboxOnly |
| editMode | edit.mode ('cell' or 'row') |
| sortModel / onSortModelChange | grid.sort.set / sort:changed |
| filterModel / onFilterModelChange | grid.filters.set and filters.quick(…, { mode: 'words' }) / filter:changed; every free-tier operator translates |
| rowSelectionModel / onRowSelectionModelChange | grid.selection.set / selection:changed, as { type: 'include', ids } by MUI id |
| columnVisibilityModel / onColumnVisibilityModelChange | layout.hidden, columns.hide/show / column:visible |
| paginationModel / onPaginationModelChange | pagination.pageSize and pagination.applyPage / page:changed - memory source only |
| pageSizeOptions | pagination.pageSizes; [25, 50, 100] by default, as in MUI - memory source only |
| onRowClick | row:clicked, as (params, event, details) |
| onCellClick / onCellDoubleClick | cell:clicked / cell:dblclicked |
| onCellEditStop / onCellModesModelChange | cell:edit:end / cell:edit:start and cell:edit:end |
| onRowEditStop / onRowModesModelChange | row:edit:end / row:edit:start and row:edit:end, in editMode="row" |
| onColumnResize | column:resized |
| onColumnOrderChange | column:moved |
| onDataSourceError | source:error |
| apiRef | apiRef.current is the six methods below while the grid exists, null after |
| slots / showToolbar | slots.toolbar rendered above the grid when showToolbar is true; any other slot is refused |
| source | Not an MUI prop. A Lattice source, for rows that are not in memory. Over one, the three paging props are refused by name |
| className / style | the host element |
| Column key | What it becomes |
|---|---|
| field | the column id and the row property |
| headerName | the heading |
| width | layout.width; 100 by default, as in MUI |
| flex | layout.flex, instead of a width |
| type | string, number, date, dateTime, boolean to the grid's types; any other is refused |
| valueGetter | value.compute, called (value, row, column, apiRef) |
| valueFormatter | value.format, called the same way |
| renderCell | a cell renderer: text as text, a React element through its own root (needs ReactDOM) |
| editable | edit |
| sortable | sort: false |
| filterable | filter: false |
apiRef method | What it does |
|---|---|
| getSelectedRows() | a Map of MUI id to row, from grid.selection.rows() |
| setSortModel(model) | grid.sort.set; onSortModelChange fires |
| setFilterModel(model) | the filter translation; onFilterModelChange fires once |
| scrollToIndexes({ rowIndex, colIndex }) | grid.scroll.toCell/toRow/toColumn; the checkbox column is index 0, as in MUI |
| exportDataAsCsv(options) | grid.export.csv({ download: true }): the selected rows if any, else the filtered rows; fileName, delimiter, includeHeaders, utf8WithBom, fields, allColumns, escapeFormulas mapped |
| updateRows(updates) | merge by id, add a new id, _action: 'delete' removes; _action: 'replace' is refused |
Refused by name, with a reason of its own: paginationMode,
rowCount, paginationMeta and onPaginationMetaChange (MUI's
server paging hands the paging contract to the host; the grid pages a non-memory source inside
the source); dataSource (use a Lattice source); the Pro/Premium props
rowGroupingModel, onRowGroupingModelChange, groupingColDef,
rowGroupingColumnMode, disableRowGrouping, treeData,
getTreeDataPath, onRowExpansionChange, aggregationModel,
onAggregationModelChange and aggregationFunctions (not verified against
the Pro types, so not guessed at); sx and classes; and
initialState (pass the controlled models instead). Any other prop, column key,
slot, column type, filter operator or CSV option is refused with the same warning family, by its
own name. A filter model with one untranslatable operator is not applied at all.
Client-side paging over a memory source only. MUI pages rows the browser
already holds, which is what the grid's own paging does over a memory source - so there the
three paging props map one for one, and MUI's defaults (100 a page, [25, 50, 100]) apply.
Over a pushdown, paged or remote source the page belongs to the source:
paginationModel, onPaginationModelChange and pageSizeOptions
are each refused by name and the rows are left exactly as the source delivers them.
Controlled models follow the adapter's identity rule. sortModel,
filterModel, rowSelectionModel, columnVisibilityModel and
paginationModel are applied when their identity changes, and their callbacks fire for
the reader's and apiRef's changes, never for the prop's own - MUI's rule. One
difference, stated: the grid is not pinned to a controlled model between renders, so a host that
ignores onSortModelChange sees the reader's sort until it next passes a
sortModel, where MUI would snap it back.
const { createTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid, createPushdownSource } = await import('../packages/dom/src/index.js');
const { createLatticeDataGrid, createDataGridController } = await import('../packages/modules/react/index.js');
// A data-grid page's rows and column definitions, unchanged.
const rows = [
{ code: 'a', name: 'Ada', age: 36, first: 'Ada', last: 'Lovelace', ok: true },
{ code: 'b', name: 'Bob', age: 20, first: 'Bob', last: 'Moog', ok: false },
{ code: 'c', name: 'Cy', age: 50, first: 'Cy', last: 'Young', ok: true },
];
const columns = [
{ field: 'code', headerName: 'Code', width: 80, sortable: false, filterable: false },
{ field: 'name', headerName: 'Name', flex: 1, editable: true },
{ field: 'age', type: 'number', valueFormatter: (value) => `${value} yrs` },
{ field: 'full', valueGetter: (value, row) => `${row.first} ${row.last}`,
renderCell: (params) => String(params.value).toUpperCase() },
{ field: 'ok', type: 'boolean' },
];
// Every callback records its own name, so the result says which fired.
const heard = [];
const hear = (name) => () => heard.push(name);
const apiRef = { current: null };
// The framework-free half, driven here with no React at all.
const grid = createDataGridController({
createGrid,
element: createTestDom().root,
props: {
rows, columns, getRowId: (row) => row.code, loading: false, density: 'compact',
checkboxSelection: true, disableRowSelectionOnClick: true, editMode: 'cell',
sortModel: [{ field: 'age', sort: 'desc' }], onSortModelChange: hear('sort'),
filterModel: { items: [] }, onFilterModelChange: hear('filter'),
rowSelectionModel: { type: 'include', ids: new Set(['b']) }, onRowSelectionModelChange: hear('selection'),
columnVisibilityModel: { ok: false }, onColumnVisibilityModelChange: hear('visibility'),
paginationModel: { page: 0, pageSize: 2 }, onPaginationModelChange: hear('page'), pageSizeOptions: [2, 5],
onRowClick: hear('row'), onCellClick: hear('cell'), onCellDoubleClick: hear('dblclick'),
onCellEditStop: hear('editStop'), onColumnResize: hear('resize'), onColumnOrderChange: hear('move'),
onCellModesModelChange: hear('cellModes'), apiRef,
},
}).grid;
flushFrames();
const shown = `rows ${grid.rows.count()} of ${grid.rows.matchCount()}, first ${grid.rows.get(0).key}, `
+ `${grid.rows.text('a', 'age')}, ${grid.rows.text('a', 'full')}`;
// The props' own values called nothing back; the reader and the apiRef do.
grid.columns.show('ok');
apiRef.current.setSortModel([{ field: 'name', sort: 'asc' }]);
apiRef.current.setFilterModel({ items: [{ field: 'age', operator: '>', value: 25 }] });
grid.selection.set(['a', 'c']);
grid.pagination.set({ page: 0, pageSize: 5 });
flushFrames();
grid.edit.start('a', 'name');
grid.edit.stop();
grid.columns.resize('name', 150);
grid.columns.move('age', 0);
const selected = [...apiRef.current.getSelectedRows().keys()].join('+');
apiRef.current.updateRows([{ code: 'c', name: 'Cyrus' }]);
const scrolled = apiRef.current.scrollToIndexes({ rowIndex: 1 });
// In a browser this downloads people.csv; Node has no download, so the grid says so.
apiRef.current.exportDataAsCsv({ fileName: 'people', fields: ['code', 'name'] });
// Row editing, with its two callbacks.
const rowHeard = [];
const rowGrid = createDataGridController({
createGrid, element: createTestDom().root,
props: { rows, columns, getRowId: (row) => row.code, editMode: 'row',
onRowEditStop: () => rowHeard.push('rowEditStop'), onRowModesModelChange: () => rowHeard.push('rowModes') },
}).grid;
flushFrames();
rowGrid.edit.start('a', 'name');
rowGrid.edit.stop();
// A pushdown source: the page model is refused by name, the error still reaches the host.
let failed = '';
const remote = createDataGridController({
createGrid, element: createTestDom().root,
props: {
columns, paginationModel: { page: 1, pageSize: 2 },
source: createPushdownSource({ adapter: { name: 'down', capabilities: {}, execute: async () => { throw new Error('offline'); } } }),
onDataSourceError: (error) => { failed = error.message; },
},
}).grid;
await new Promise((r) => setTimeout(r, 20));
// And the React component: the toolbar slot above the grid, on a host element.
const React = {
createElement: (type, props, ...children) => ({ type, props, children }),
useRef: (current) => ({ current }), useEffect() {}, useImperativeHandle() {}, forwardRef: (f) => f,
};
const DataGrid = createLatticeDataGrid({ React, createGrid });
const tree = DataGrid({ rows, columns, slots: { toolbar: 'MyToolbar' }, showToolbar: true,
className: 'people', style: { height: 300 } }, null);
return `${shown} | heard ${heard.join(',')} | selected ${selected}, ${grid.rows.byKey('c').data.name}, `
+ `scrolled ${scrolled} | row edit ${rowHeard.join(',')} | pushdown pagination `
+ `${remote.config().pagination === undefined ? 'refused' : 'applied'}, error ${failed} | `
+ `${tree.props.className} toolbar ${tree.children[0].children[0].type}`;
Vue 3: every viewer, not only the grid
Until 1.66 the Vue adapter wrapped createGrid and nothing else. Every other
shipped viewer - the KPI panel, a chart, the board, the Gantt, the layout, the tab
strip - and the data router had no Vue surface, so a Vue application wrote its own
onMounted/onBeforeUnmount pair per viewer. It does not have to any
more: there is one component per viewer, and each keeps the contract the grid component
already kept.
Every component is built with defineComponent and a render function, so the
adapter needs no template compiler in your build and runs on
vue.runtime.* - the compiler-free build a Vite application ships. The
floor is Vue 3.3.
// lattice.js - built once, at module scope.
import * as vue from 'vue';
import { createGrid } from '@toclocoinc/lattice-grid';
import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { createLatticeVue } from '@toclocoinc/lattice-grid/modules/vue';
export const L = createLatticeVue({ vue, createGrid, createKPI, createChart, createDataRouter });
// Dashboard.vue
<script setup>
import { L } from './lattice.js';
L.provideLatticeGrids();
L.provideLatticeRouter(ROUTER_CONFIG);
const rows = shallowRef([]);
</script>
<template>
<component :is="L.LatticeGrid" name="quakes" route="all" v-bind="GRID_CONFIG" :rows="rows" />
<component :is="L.LatticeKPI" grid-name="quakes" :tiles="TILES" :columns="5" />
<component :is="L.LatticeChart" grid-name="quakes" type="bar" x="region" y="count" />
</template>
| Export | Signature | What it is |
|---|---|---|
| createLatticeGrid | ({ vue, createGrid }) | The grid component. Extended in 1.66 with rowUpdates, predicates, name, route and the grid-ready / grid-destroyed emits. Unchanged for anyone already on it. |
| createLatticeKPI | ({ vue, createKPI }) | The KPI panel. Grid-bound through the provided registry by default; bind :rows instead for a panel with no grid. |
| createLatticeChart | ({ vue, createChart }) | A chart. Requires a grid, so nothing is mounted until one exists; a changed spec key goes to chart.update() and the chart redraws rather than being rebuilt. |
| createLatticeKanban | ({ vue, createKanban }) | The board. rows, quickFilter, sprint, epic, loading and error are live props. |
| createLatticeCalendar | ({ vue, createCalendar }) | The calendar. rows, view and date are live props; grid-bound through the registry by default. The reschedule events arrive as @beforeMove (cancel with preventDefault(reason?)) and @card-move; call move(key, to) on the exposed instance. |
| createLatticeGantt | ({ vue, createGantt }) | The Gantt. tasks and dependencies are live props. |
| createLatticeLayout | ({ vue, createLayout }) | The layout. Windows are driven through the exposed instance; its events arrive as @layout-changed, @window-moved and the rest. |
| createLatticeTabs | ({ vue, createTabs, createGrid? }) | The tab strip, with Vue-rendered tab content: a tab's content is a named slot - <template #overview> for a tab with id: 'overview', or <template #tab-overview> - rendered into the strip's own panel through a <Teleport>, so a tab's grid is a real LatticeGrid with props, a ref and the surrounding provides. A tab with no slot is left to the module, so the two kinds mix on one strip. |
| createLatticeDesigner | ({ vue, createDesigner }) | The designer. mode, state, sources and guardrails are live; relationships, factories and llm are mount-time - change the component's key to apply one. state is controlled when bound: a new state (by identity) is pushed in, and designer changes are emitted as @state, @mode, @page and @select, never as a write to the prop. The template ref is the live Designer handle. |
| createLatticeGridContext | ({ vue }) | Returns { LatticeGridProvider, provideLatticeGrids, useLatticeGrid }. A grid-bound viewer needs the grid instance, which appears after the first render; a template ref cannot help, because assigning to one re-renders nobody. Call provideLatticeGrids() in a parent's setup, or wrap the subtree in <LatticeGridProvider>. Several grids may publish under one registry - name on the grid, grid-name on the viewer. |
| createLatticeRouter | ({ vue, createDataRouter }) | Returns { provideLatticeRouter, useLatticeRouter, LatticeRouterProvider }. provideLatticeRouter(config) is called in a component's setup: the router is built by the first grid under it that names a route, and destroyed when that component's scope is. The config is read once, because rebuilding would drop every attached grid and every row held. |
| createLatticeViewer | ({ vue, viewer, mount, … }) | The generic behind all of the above, and the escape hatch for a viewer with no named factory yet. |
| createLatticeVue | ({ vue, …factories }) | Every binding from one call: pass the factories the application uses and it builds those components, leaving the rest undefined. Nothing is imported, so an application that never uses the board never loads the board. |
| createViewerController | ({ viewer, mount, element, props }) | The framework-free viewer lifecycle - mount once, push changed props into the live instance, destroy - shared by every adapter. |
| VIEWER_EVENTS | Record<string, readonly string[]> | Every event each non-grid viewer emits, keyed by viewer name. |
| VIEWER_APPLY | Record<string, Record<string, Function>> | Which props each viewer can take live, and the instance call each becomes. Anything not listed is mount-time configuration. |
| viewerHandlerName | (event) => string | A viewer event as the handler key the shared controller dispatches on: card:move becomes onCardMove. A Vue template binds the dashed form instead - @card-move. |
| DEFAULT_GRID_NAME | string | The name a grid publishes itself under when you do not choose one: default. |
const A = await import('../packages/modules/vue/index.js');
// A stand-in for Vue. The adapter never imports it, so a factory only needs
// the handful of names it destructures - which is exactly why this example
// runs in Node with no framework installed.
const vue = {
defineComponent: (d) => d, h: () => ({}), ref: (v) => ({ value: v }),
shallowRef: (v) => ({ value: v }), computed: (f) => ({ get value() { return f(); } }),
watch: () => () => {}, onMounted: () => {}, onBeforeUnmount: () => {},
onScopeDispose: () => {}, provide: () => {}, inject: () => null,
Teleport: Symbol('Teleport'), Fragment: Symbol('Fragment'),
};
const stub = () => ({ on: () => () => {}, setRows: () => {}, destroy() {} });
// One call builds whichever components the factories you pass support.
const L = A.createLatticeVue({
vue, createGrid: stub, createKPI: stub, createChart: stub, createDataRouter: stub,
});
// …or one factory at a time. Each returns a Vue component definition.
const components = [
A.createLatticeGrid({ vue, createGrid: stub }),
A.createLatticeKPI({ vue, createKPI: stub }),
A.createLatticeChart({ vue, createChart: stub }),
A.createLatticeKanban({ vue, createKanban: stub }),
A.createLatticeCalendar({ vue, createCalendar: stub }),
A.createLatticeGantt({ vue, createGantt: stub }),
A.createLatticeScheduler({ vue, createScheduler: stub }),
A.createLatticeLayout({ vue, createLayout: stub }),
A.createLatticeTabs({ vue, createTabs: stub }),
A.createLatticeForm({ vue, createForm: stub }),
].filter((c) => typeof c.setup === 'function');
// The generic behind them, the registry pair and the router composables.
A.createLatticeViewer({ vue, viewer: 'kpi', mount: stub });
const { LatticeGridProvider, useLatticeGrid } = A.createLatticeGridContext({ vue });
const { provideLatticeRouter } = A.createLatticeRouter({ vue, createDataRouter: stub });
// Events are bound under dashed names, because a colon is directive syntax.
const gridEvent = A.dashedName(A.EVENT_NAMES.find((n) => n === 'cell:changed'));
const viewerEvent = A.dashedName(A.VIEWER_EVENTS.kanban.find((n) => n === 'card:move'));
A.viewerHandlerName('card:move');
// And the lifecycle underneath, driven with no framework at all: mount once,
// push a changed live prop into the instance that already exists, destroy.
const seen = [];
const controller = A.createViewerController({
viewer: 'kpi',
element: {},
props: { rows: [{ id: 1 }] },
mount: () => ({ setRows: (r) => seen.push(r.length), on: () => () => {}, destroy() {} }),
});
controller.update({ rows: [{ id: 1 }, { id: 2 }] });
controller.destroy();
void [L.LatticeGrid, LatticeGridProvider, useLatticeGrid, provideLatticeRouter, A.VIEWER_APPLY];
return `${components.length} components, ${gridEvent}, ${viewerEvent}, `
+ `${A.DEFAULT_GRID_NAME}, rows ${seen.join('/')}`;
The grid paints outside Vue's reactivity, and that is deliberate. The
adapter creates the grid in onMounted and pushes changed props into it; the grid
then renders on its own schedule, not Vue's. So nextTick() tells you Vue has
patched the DOM, not that the grid has painted. Measure, screenshot or scroll in a
grid event - @render-done, @rows-changed,
@size-changed - rather than after nextTick or in
onMounted.
Hold big row arrays in a shallowRef, never a ref.
ref([...100k rows]) makes every row object a reactive proxy: Vue walks the array
on assignment and every property read on every row goes through a proxy trap from then on.
The grid neither needs nor wants that - it re-reads the array itself when the reference
changes. Use shallowRef(rows) and assign a new array when the data
changes (rows.value = next), or markRaw the objects. The same goes
for the grid instance you keep off a template ref: a ref would proxy the whole
Grid, and the proxy is not === the object
createGrid returned.
Props are diffed with Object.is, and that is a promise about identity.
An inline :columns="[{ field: 'a' }]" is a new array on every render, so it
counts as changed every render and reconfigures the grid every render. Hoist it to module
scope or put it behind a computed. This is not a defect to work around: a deep
compare of a million-row array on every render would cost more than the reload it prevents.
For the same reason nothing here uses a { deep: true } watcher - one over a
config carrying rows would walk every row object on every tick.
Live props versus mount-time props. The grid takes any changed configuration
key through one call (grid.setAll); no other viewer does. So each viewer declares
which props it can take while it is running (VIEWER_APPLY), and everything else
is mount-time configuration. A mount-time prop that changes is not silently ignored
and not silently remounted (that would throw away scroll position, selection and
expansion): it is named once in a warning that tells you to give the component a
:key that changes when the rebuild is wanted, or to drive the instance through
the ref. The tab strip's whole tabs array is mount-time for the same reason - the module has no way to repaint an existing tab's label or badge.
Events are dashed; the lifecycle pair is grid-ready and
grid-destroyed. cell:changed is emitted as
@cell-changed, because a colon in a Vue binding is directive syntax. Every grid
event is declared in emits, so none of them falls through onto the host element.
The two lifecycle emits are named grid-* because ready and
destroy are grid event names already re-emitted here, and the React and Angular
adapters name their pair the same.
Reaching the instance, and class/style/id.
A template ref on the grid exposes grid()
(gridRef.value.grid()); every other component exposes
instance(). Both are null before mount. class,
style and id are applied to the component's own host element rather
than passed on as configuration; every other binding is the viewer's configuration, with a
known kebab-cased key mapped to its camelCase form (:row-key reaches
rowKey), as the web component does.
Cleanup, and no SSR. Every component destroys its instance, stops its
watchers and unsubscribes its listeners in onBeforeUnmount, so a
v-if toggled twice leaves exactly one instance alive and an unmounted page
leaves nothing on a timer or an event bus. The router is destroyed with the component whose
setup provided it. Every component owns a real DOM element and builds nothing
until onMounted, so a server render emits the empty host and nothing else: there
is no SSR or Nuxt server-side support, and Vue 2 is not supported.
Svelte 5: components you compile, not a bundle you load
Until 1.66 the Svelte adapter was a single action, use:lattice, over a grid.
An action cannot do what the other adapters do - it takes a node and a value, so it has
no children, no snippets and no context of its own, and a tab strip whose panels hold
Svelte-rendered content is simply out of its reach. So 1.66 adds components,
one per viewer, and they ship the way the Svelte ecosystem ships components: as
.svelte source, compiled by your own toolchain.
svelte is an optional peer dependency; the package still installs
nothing. Svelte 5 only - the components are written in runes and
snippets, neither of which exists in Svelte 4. The action is unchanged and still supported.
<!-- Dashboard.svelte -->
<script>
import Provider from '@toclocoinc/lattice-grid/svelte/GridProvider.svelte';
import Grid from '@toclocoinc/lattice-grid/svelte/Grid.svelte';
import KPI from '@toclocoinc/lattice-grid/svelte/KPI.svelte';
import Chart from '@toclocoinc/lattice-grid/svelte/Chart.svelte';
import Tabs from '@toclocoinc/lattice-grid/svelte/Tabs.svelte';
let rows = $state.raw([]);
let board;
</script>
<Provider>
<Grid bind:this={board} name="quakes" class="grid" {...GRID_CONFIG} {rows}
onCellChanged={(e) => save(e)} />
<KPI gridName="quakes" tiles={TILES} columns={5} />
<Chart gridName="quakes" type="bar" x="region" y="count" />
<Tabs tabs={TABS} active={open} onTabChanged={(e) => (open = e.id)}>
{#snippet all()}<Grid name="all" {...GRID_CONFIG} {rows} />{/snippet}
</Tabs>
</Provider>
| Component | Import | What it is |
|---|---|---|
| Grid.svelte | @toclocoinc/ | One grid for the component's lifetime. Every configuration key is a prop, diffed with Object.is and pushed through setAll; rowUpdates is a keyed diff straight to rows.apply(); predicates are named filters registered through filters.where, so they compose with the reader's own. bind:this plus grid() hands you the live Grid. |
| GridProvider.svelte | …/ | Puts a grid registry in context. A grid publishes itself under its name; a viewer finds it by gridName. Also available as createGridRegistry() for a host that would rather call setContext itself. |
| KPI, Chart, Kanban, Gantt, Layout | …/ | One viewer each, built once and updated in place. A grid-bound viewer waits for its grid rather than mounting empty, and a different grid rebuilds it. bind:this plus instance() hands you the live viewer. |
| Tabs.svelte | …/ | The tab strip, with Svelte-rendered panels: a tab's content is a {#snippet} named after the tab's id, so a tab's grid is a real component with props, context and a reference. A tab with no snippet is left to the module, so configuration-driven tabs still work and the two kinds mix. |
| Router.svelte | …/ | A Data Router in context, built by the first grid that names a route and destroyed with the component. |
| Designer.svelte | …/ | The designer. mode, state, sources and guardrails are live; relationships, factories and llm are mount-time. state is controlled when passed. bind:this plus instance() hands you the live Designer. |
The components are deliberately thin - a template, the $props()
plumbing, a bind:this getter and the context wiring - because they ship as source
you read. Every rule they follow lives in modules/svelte, which ships minified,
and each is exported so you can build a component of your own on exactly the same footing:
| Export | Signature | What it does |
|---|---|---|
| bindGrid | ({ createGrid, element, props, registry, router }) | Owns one grid: builds it once, diffs props into it through setAll, registers predicates before it publishes, applies each new rowUpdates object through rows.apply(), attaches it to the router, and on destroy detaches and withdraws before destroying. |
| bindViewer | ({ viewer, factory, element, registry }) | Owns one KPI panel, chart, board, plan or layout. sync(props) builds it once everything it needs has arrived, pushes changed props into the live instance, and rebuilds it only when it is bound to a different grid. |
| bindDesigner | ({ createDesigner, element }) | Owns one designer. sync(props) builds it once, then pushes changed props into the live instance; destroy() tears it down. Built over createDesignerController, the same lifecycle the other adapters drive. |
| createDesignerController DESIGNER_EVENTS designerHandlerName | shared with React and Vue | The framework-free designer lifecycle, the four designer events, and the event-to-prop name mapping - re-exported here so a Svelte host can build its own component on the same footing. |
| bindTabs | ({ createTabs, element, createGrid, onPanels }) | Owns one tab strip and claims a panel for each tab the host rendered content for. place(id, holder) moves that tab's display: contents holder into the panel - Svelte has no portal, and moving one container is what keeps the subtree a genuine child of the component that declared it. |
| createGridRegistry | () | The registry GridProvider.svelte puts in context: publish, get, names and a subscribe a component turns into its own reactivity. |
| createRouterHandle | ({ createDataRouter, config }) | A Data Router built on the first grid that asks to be routed and destroyed with the component that provided it. config may be a function, read at build time. |
| adaptProps | (kind, props) | Splits a component's props into the instance's configuration and the adapter's own, dropping class/style/id, resolving row-key-style aliases, and normalising a callback prop written either way. |
| pickCallback | (props, name) | A callback prop under either spelling: onGridReady or ongridready. |
| viewerMount | (viewer, factory) | The call each module's factory actually takes - three want (element, config), the chart wants a container in its options and the Gantt an element. |
| snippetTabIds | (tabs, props) | Which tabs the host declared a snippet for, in declaration order. |
| VIEWER_BINDING | Record<string, {…}> | How each viewer is wired: its label, the config keys without which nothing is built, and the key its bound grid is passed under. |
| GRID_REGISTRY_KEY ROUTER_KEY | symbol | The context keys the registry and the router handle are published under. Registered symbols (Symbol.for), so two components imported from different files meet the same registry. |
const S = await import('../packages/modules/svelte/index.js');
// A component hands its props straight to the helpers. They keep the
// adapter's own props out of the instance and accept a callback under
// either Svelte 5 spelling.
const split = S.adaptProps('grid', {
rowKey: 'id', 'class': 'grid', name: 'main', oncellchanged: () => {},
});
const ready = S.pickCallback({ ongridready: () => 'called' }, 'onGridReady');
// The registry a <GridProvider> puts in context, and the grid binding a
// <Grid> drives. Both are plain functions: no Svelte here at all.
const registry = S.createGridRegistry();
const applied = [];
const fakeGrid = () => ({
setAll: (c) => applied.push(c.theme), destroy() {},
on: () => () => {}, rows: { apply: () => {} },
filters: { where: () => {} },
});
const bound = S.bindGrid({
createGrid: fakeGrid, element: {}, registry, props: { name: 'main', theme: 'light' },
});
bound.update({ name: 'main', theme: 'dark' });
// A viewer waits for its grid, then is built once. VIEWER_BINDING says
// which key its grid arrives under and what it cannot be built without.
const built = [];
const stub = () => ({ on: () => () => {}, setRows: () => {}, update: () => {}, destroy() {} });
for (const viewer of Object.keys(S.VIEWER_BINDING)) {
const panel = S.bindViewer({
viewer, element: {}, registry,
factory: (...args) => { built.push(viewer); return stub(...args); },
});
panel.sync({ gridName: 'main' });
panel.destroy();
}
// A tab strip claims a panel for each tab the host wrote a snippet for.
const tabs = [{ id: 'one' }, { id: 'two' }];
const props = { one: () => {} };
const strip = S.bindTabs({
element: {},
createTabs: (el, config) => ({ config, on: () => () => {}, destroy() {} }),
});
strip.sync({ tabs, contentIds: S.snippetTabIds(tabs, props) });
const claimed = strip.instance.config.tabs.filter((t) => t.view).map((t) => t.id);
// One router, built by the first grid that asks and destroyed with the
// component that provided it.
let routers = 0;
const handle = S.createRouterHandle({
createDataRouter: () => { routers += 1; return { attach: () => {}, detach: () => {}, destroy: () => {} }; },
});
handle.attach({}, 'all'); handle.attach({}, 'all'); handle.destroy();
// And the call each module's factory actually takes.
S.viewerMount('chart', (opts) => opts)({}, { type: 'bar' });
void [S.GRID_REGISTRY_KEY, S.ROUTER_KEY, ready()];
return `config ${Object.keys(split.config).join(',')}; own ${split.own.name}; `
+ `registry ${registry.names().join(',')}; grid ${applied.join(',')}; `
+ `tabs ${claimed.join(',')}; viewers ${built.join(',')}; router ${routers}`;
Events are callback props. Svelte 5 removed component events, so
cell:changed arrives as onCellChanged - the same name React
uses. The all-lowercase oncellchanged is accepted too, for anyone who prefers
Svelte's DOM attribute style. The grid's own dashed names (cell-changed) still
apply to the use:lattice action, which dispatches CustomEvents on
the node.
Reactivity, and rows. The grid paints outside Svelte's scheduler: it owns
its element and its own render loop, and nothing inside it is reactive state. Hold large row
arrays in $state.raw (or in an ordinary let), never in deep
$state - a proxy over a million rows costs far more than the reload it
would save, and the components diff by identity, so a new array is what signals a change.
An inline literal prop (columns={[…]}) is a new array on every render and
reconfigures the viewer every time; hoist it.
Cleanup, and no SSR. Every component builds its instance in
onMount and destroys it in onDestroy, unsubscribing its listeners,
withdrawing its grid from the registry and detaching from the router before the grid goes.
Nothing is built during server rendering - SvelteKit SSR emits the empty host element
and nothing else - so these components are browser-only by construction.
Angular: a compiled entry point, @toclocoinc/lattice-grid/angular
Angular's components are not objects a library can assemble at run time in a production
build. modules/angular did assemble them that way, which needs Angular's JIT
compiler in the page - present on a development server, absent from every AOT build.
So Angular gets an entry point of its own: TypeScript components compiled by
@angular/compiler-cli into a partial-Ivy library, which your build's Angular
Linker turns into definitions exactly as it does for any other Angular library you install.
No compiler in your bundle, and one standalone component per viewer.
It ships inside the grid package, at the subpath
@toclocoinc/lattice-grid/angular: one install, one version number, one
licence. @angular/core and @angular/common are optional peer
dependencies of the package, so a project that is not an Angular one installs nothing extra
and sees no warning.
npm install @toclocoinc/lattice-grid
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import {
LatticeGridComponent, LatticeKpiComponent, LatticeChartComponent, provideLattice,
} from '@toclocoinc/lattice-grid/angular';
@Component({
selector: 'app-dashboard',
imports: [LatticeGridComponent, LatticeKpiComponent, LatticeChartComponent],
template: `
<lattice-grid #grid name="quakes" [config]="config" [quickFilter]="search()"
(cell-changed)="save($event)" />
<lattice-kpi gridName="quakes" [config]="{ tiles }" />
<lattice-chart gridName="quakes" [config]="{ type: 'bar', x: 'region', y: 'count' }" />
`,
})
export class Dashboard {
// The live grid, the same object createGrid returns.
grid = viewChild<LatticeGridComponent>('grid');
}
// Each factory is injected, not imported by the library: an application that
// shows a grid downloads the grid, and never the board or the eighteen charts.
bootstrapApplication(Dashboard, {
providers: [provideLattice({ createGrid, createKPI, createChart })],
});
| Export | Element | What it is |
|---|---|---|
| LatticeGridComponent | <lattice-grid> | The grid. [config] is every configuration key; [sort], [filters], [quickFilter] and [selectedKeys] are applied through the matching API; [rowUpdates] and [predicates] drive a live feed. Every grid event is an output under its kebab-case name, and (grid-ready) hands you the instance. Give the element a height. |
| LatticeGridDirective | [latticeGrid] | The same component on an element your template already owns: <div [latticeGrid]="config" class="tall"></div>. Same inputs, outputs and grid reference. |
| LatticeKpiComponent | <lattice-kpi> | The KPI panel. Grid-bound by default - [gridName] picks which published grid - or give it [rows] for a panel with no grid. |
| LatticeChartComponent | <lattice-chart> | A chart. Requires a grid, so nothing is built until one exists; a changed spec key goes to chart.update() and the chart redraws rather than being rebuilt. (click), (hover) and (leave) on this element are the chart's events, carrying the datum under the pointer. |
| LatticeKanbanComponent | <lattice-kanban> | The board. [rows], [quickFilter], [sprint], [epic], [loading] and [error] are live inputs. |
| LatticeCalendarComponent | <lattice-calendar> | The calendar. [rows], [view] and [date] are live inputs; grid-bound through the registry by default. The reschedule events arrive as (beforeMove) (cancel with preventDefault(reason?)) and (card-move); call move(key, to) on the component. |
| LatticeGanttComponent | <lattice-gantt> | The plan. [tasks] and [dependencies] are live inputs. It takes an explicit [grid] but never adopts a published one. |
| LatticeLayoutComponent | <lattice-layout> | The dashboard layout. Windows are driven through the instance; its events arrive as (layout-changed), (window-moved) and the rest. |
| LatticeDesignerComponent | <lattice-designer> | The designer. [mode], [state], [sources] and [guardrails] are live inputs; [relationships], [factories] and [llm] are mount-time. [state] is controlled when bound. The events are (stateChange), (modeChange), (pageChange) and (selectChange); (ready) hands you the live Designer. |
| LatticeTabsComponent LatticeTabDirective | <lattice-tabs> ng-template[latticeTab] | The tab strip, with Angular-rendered tab content: a tab's content is an <ng-template latticeTab="id"> in your own template, rendered into the strip's panel through this component's ViewContainerRef - so a tab's grid is a real <lattice-grid> with inputs, a reference and your injectors above it. A tab with no template is left to the module, so configuration-driven grid tabs still work and the two kinds mix on one strip. |
| LatticeGridRegistry | inject(LatticeGridRegistry) | Where <lattice-grid name="…"> publishes itself and grid-bound viewers find it, as a signal per name - a panel declared before its grid exists mounts itself the moment the grid arrives. providedIn: 'root'; put it in a component's own providers to scope a registry to that subtree. |
| provideLattice | provideLattice({ …factories }) | The factories the components build through. Give it the ones your application uses; a component whose factory is missing names the import and the provider call that fixes it. |
| provideLatticeRouter LatticeRouter | providers: [provideLatticeRouter(cfg)] | The data router as a service. Put it in a component's providers and it is created when the first <lattice-grid route="…"> under it attaches, and destroyed with that component - its configuration read once, because rebuilding would drop every attached grid and every row it holds. Each grid detaches before it is destroyed, so the router never holds a dead grid. |
Change detection: zone or zoneless, unconfigured. The grid is created inside
NgZone.runOutsideAngular, because it installs its own scroll, wheel and pointer
listeners and running change detection on every scroll frame of a million-row grid is the
difference between smooth and unusable. An event that reaches an output you have
bound re-enters the zone, so (cell-changed)="count = count + 1" repaints
exactly as you expect; an output nobody bound costs nothing. Under zoneless change detection
that machinery is Angular's own no-op and a signal you set in a handler repaints the view.
The one thing to know when reading the grid's DOM from Angular: the grid paints on its
own schedule, not Angular's, so measure a cell in a grid event, not in
ngAfterViewInit.
OnPush is safe everywhere. None of these components asks its
parent to re-render: each owns one element, builds one instance in
afterNextRender and pushes changed inputs into it from ngOnChanges.
A host on OnPush that never re-renders still gets a fully live grid, because the
grid is not rendered by Angular.
Inputs are diffed by identity, and never rebuild the instance. A changed
input reaches the viewer that is already on screen - scroll position, selection,
expansion and any open editor intact. Two things rebuild rather than update: the grid a
viewer is bound to, and an input it cannot exist without. An input a viewer has no live
setter for is named once in a warning rather than silently dropped. And because the
comparison is Object.is, an inline [config]="{ rows: rows }" is a
new object on every pass: hold it in a field or a signal.
Cleanup is the component's. ngOnDestroy detaches from the
router, withdraws the grid from the registry and destroys the instance - in that order.
An @if that closes and opens again leaves exactly one instance alive, and
destroying the application leaves no instance, interval or listener behind. That is asserted
with counters in a real browser, against the linked package, in
test/angular-package-browser.test.js.
Angular 17 and up, browser only. The library is compiled partially, so your
own Angular version compiles it: its declarations need a linker no newer than 14, and its peer
range is >=17. Nothing is created on the server - isPlatformBrowser guards every build and afterNextRender does not
run there - so a server-rendered page emits the empty host element and the grid is built
on hydration. Angular Universal is not otherwise supported.
modules/angular is deprecated. The old bundle still works where
it always worked - a page with @angular/compiler loaded - and it now
says so once, and fails with a [lattice] message naming this package when it
finds a real Angular with no JIT compiler, instead of leaving you with Angular's own. In
1.65 it also gained the fix that <div [latticeGrid]="config"> binds the
configuration through the directive's selector, as its documentation always said it did. It
will be removed in a later release; move to @toclocoinc/lattice-grid/angular,
which covers every viewer rather than the grid alone and needs no second install.
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. The exported name itself cannot be reassigned to wrap it - see the note below. |
| createHeadlessGrid(config?) | Grid | Core only. Everything below except grid.element and the DOM-only config keys works unchanged. What that does and doesn't reach is spelled out below. |
| defaults(config?) | object | House-wide defaults, merged beneath the config of every grid built afterwards, through either factory. The per-grid value always wins. defaults() reads the current set; defaults(null) clears it. See the note below. |
createGrid cannot be monkey-patched. Wherever it is exported - window.LatticeGrid.createGrid from the UMD build, or the named import from the ESM
build - it is defined with Object.defineProperty(..., { get, enumerable: true })
and no setter, and configurable defaults to false because the
descriptor never sets it. Assigning to it in an ordinary (non-strict) script is not an error:
the assignment is simply discarded and LatticeGrid.createGrid still returns the
original function. In a module or any script under 'use strict' - which every ES
module is - the same assignment throws TypeError: Cannot set property createGrid of
[object Object] which has only a getter. Either way, a house-wide patch applied this way
has no effect, and in the sloppy-mode case nothing tells you it didn't. There is no supported
way to replace the function in place. The supported pattern is a factory your own code owns:
// your-lattice.js - the one place that knows your house defaults
import { createGrid as baseCreateGrid } from '@toclocoinc/lattice-grid';
export function createGrid(element, config) {
return baseCreateGrid(element, { theme: 'house', density: 'compact', ...config });
}
// everywhere else
import { createGrid } from './your-lattice.js';
The wrapping function above is still a good seam when the wrapper does more than supply
options. When all it does is supply options, use defaults() instead: it applies to
every grid built afterwards through either factory, including the ones built for you
inside a framework adapter or a module, which a wrapper in your own code never reaches.
- The per-grid config always wins. Defaults sit beneath what the factory
is passed: a key the grid names keeps the grid's value, a key it omits takes the house one.
A key passed as
undefinedmeans "say nothing" - as it does everywhere else in the config surface - and so takes the house value rather than blanking it. - Plain objects deep-merge; arrays and everything else replace. A house
views: { storage }and a grid'sviews: { local: true }both survive. A grid'scolumnsarray replaces the house one rather than extending it. Which keys behave as option bags follows from the value at the key, not from a fixed list. - Calling it again replaces the set, it does not accumulate, so the
result never depends on the order your modules load. Extend explicitly with
defaults({ ...defaults(), density: 'compact' }). - Never retroactive. The merge happens as a grid is built, so a grid that already exists is never revisited. Nothing reached from the defaults is shared between two grids: nested objects and arrays are copied per grid.
const { createHeadlessGrid, defaults } = await import('../packages/core/src/index.js');
// One place says what every grid in this application starts from.
defaults({ rowKey: 'id', selection: 'multiple' });
// This grid says nothing about rowKey, so the house value applies.
const a = createHeadlessGrid({
columns: [{ field: 'id' }, { field: 'name' }],
rows: [{ id: 'a1', name: 'Ada' }],
});
const house = a.rows.byKey('a1').data.name;
// This one names its own rowKey. The grid's own config always wins.
const b = createHeadlessGrid({
rowKey: 'sku',
columns: [{ field: 'sku' }],
rows: [{ sku: 's9', id: 'ignored' }],
});
const own = b.rows.byKey('s9') !== undefined && b.rows.byKey('ignored') === undefined;
a.destroy();
b.destroy();
defaults(null); // clear: grids built after this are unaffected
return `${house}; own-key ${own}; cleared ${JSON.stringify(defaults())}`;
What createHeadlessGrid covers, and what it cannot. It builds
the same core the DOM build attaches a renderer to, so everything that is not the renderer
itself is exercised exactly as it runs in a browser:
- Covered: data (
rows,columns), state (grid.state, saved views), sort, filter, group, total and pivot, formulas and computed columns, editing and optimistic write-back, export, and every event the grid emits. - Not covered: the DOM renderer, layout and measurement (column widths, row heights,
scrolling), focus, and anything whose behaviour depends on a real box being painted on
screen -
grid.elementisnulland there is nothing to measure.
See How it works in the guide for the two specifics that have cost real debugging time: a grid mounted where it has no rendered box, and what the in-repo test DOM stub does and does not stand in for.
<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. |
| filter-row | filterRow | Boolean attribute. Draws the inline filter row under the headings. |
| 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.
Web Components: every viewer, not only the grid
Since 1.66 the same bundle registers one element per viewer, so a page
without a bundler gets the KPI panel, a chart, the board, the Gantt, the dashboard layout, the
tab strip and the Data Router as tags. The grid is already registered on import;
defineLatticeElements registers the rest from the module factories the page has
imported, because nothing is imported by this module - a page that draws no charts never loads
the charts bundle.
// Every tag this package can register, under the default prefix.
const { ELEMENT_TAGS } = await import('../packages/modules/webcomponent/elements.js');
return Object.keys(ELEMENT_TAGS).length;
<script type="module">
import { createKPI } from './dist/modules/kpi.esm.js';
import { createChart } from './dist/modules/charts.esm.js';
import { defineLatticeElements } from './dist/modules/webcomponent.esm.js';
defineLatticeElements({ createKPI, createChart });
</script>
<lattice-grid name="quakes" row-key="id"></lattice-grid>
<lattice-kpi grid-name="quakes"></lattice-kpi>
<lattice-chart grid-name="quakes" type="bar" x="place" y="mag"></lattice-chart>
| Tag | Viewer | Notes |
|---|---|---|
| <lattice-grid> | createGrid | Registered on import. Gains name, route, route-options, the predicates and rowUpdates properties, and grid-ready / grid-destroyed. |
| <lattice-kpi> | createKPI | Grid-bound by default; set rows for a standalone panel. tiles is a property. Like every grid-bound viewer it waits for its grid rather than mounting empty, so a panel written above its grid is built once, when the grid arrives. |
| <lattice-chart> | createChart | Always drawn from a grid, so it waits for one. Every other attribute is the chart spec and a change goes into chart.update(). |
| <lattice-kanban> | createKanban | Board. rows, columns, card, facets are properties. |
| <lattice-calendar> | createCalendar | Calendar. rows, card and header are properties; view, date and start-property are scalars. The reschedule events arrive as lattice-beforeMove (cancel on event.detail) and lattice-card-move; call move(key, to) on the element. |
| <lattice-gantt> | createGantt | tasks and dependencies are properties and both are live. |
| <lattice-layout> | createLayout | windows is a property. |
| <lattice-tabs> | createTabs | A tab's content is a <template data-tab="‹id›"> child, cloned into the panel the module creates. With no tabs property the descriptors are read from the templates. A tab with no template is left to the module; the two kinds mix. |
| <lattice-router> | createDataRouter | A grouping element. Owns one Data Router, built by the first <lattice-grid route="…"> beneath it and destroyed on disconnect, and the name registry its descendants resolve grid-name through. |
Attributes for scalars, properties for structures. An
attribute is a string, so columns, rows, tiles,
tasks, windows and a geometry pack are properties. Scalars work as
either: an empty attribute is true, true/false are
booleans, a finite number is a number, a value starting [ or { is
JSON, and anything else is the string - so use the property when the type matters. On the viewer
elements a hyphenated attribute reaches the camelCase key (column-property →
columnProperty). Every configuration key has a real property, including one no
table here names: assign it and the element adopts it. <lattice-grid> keeps
its own attribute table above - each of those is a property too
(el.theme, el.rowKey) - and takes everything outside it through
config.
Upgrade order. A property assigned before the element
upgrades is honoured. Set your configuration before the element connects - document.createElement, assign, then append - so the viewer is built once with
everything it needs rather than built empty and reconfigured; in markup, use the JSON attribute
forms. Cleanup is on disconnect, deferred by a microtask so moving an element in
the DOM keeps its instance rather than rebuilding it. SSR: every element owns a
real DOM node, so nothing is built until it is connected in a browser; a server render emits the
empty tag and no more.
Events. Instance events are CustomEvents
named lattice- plus the event name with colons hyphenated
(card:move → lattice-card-move); the prefix is load-bearing,
because the grid emits scroll and a chart emits click. The lifecycle
events are this adapter's own and are unprefixed: grid-ready /
grid-destroyed on the grid and ready / destroyed on every
viewer, each carrying the instance in detail. The instance is also on
el.grid / el.instance.
Inside a shadow root
The elements are light DOM, deliberately. A host that puts them
inside a shadow root of its own is a different matter: the theme, each module's injected
stylesheet and the grid's generated one all live in document.head and none of them
crosses a shadow boundary, so a grid in there used to render structurally perfect and completely
unstyled. Every element now calls adoptLatticeStyles on its own root when that root
is a shadow root, mirroring each sheet as a constructable CSSStyleSheet and keeping
it level as the grid compiles new classes and as a module injects its own. A host that manages
its own adoptedStyleSheets can take the sheets directly with
latticeStyleSheets(); lattice-styles="off" on an element opts out. A
stylesheet that cannot be read as text - a cross-origin <link>, or one
carrying @import - is cloned into the root as a <link> instead.
const root = host.attachShadow({ mode: 'open' });
adoptLatticeStyles(root);
root.append(document.createElement('lattice-grid'));
Grid compatibility wrapper
A Grid class shaped like a common grid widget API (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 object model.
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 the source API'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 the source API'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 the source API's genuine RangeSelection module. |
Which compatibility keys are translated
A key outside these lists is not silently dropped: the wrapper names it
through warnOnce at construction, so a migration is told what did not come
across rather than discovering it later.
| Constructor key | Becomes |
|---|---|
| rowKey | The grid's rowKey. Defaults to id. |
| columns | Translated column by column; see the next table. |
| data | The initial rows. |
| autoHeight | autoHeight. |
| rowHeight | rowHeight. |
| headerRowHeight | headerHeight. |
| multiselection | selection: 'multiple'. |
| dragItem | rowReorder when set to row. It does not also enable rowTransfer: the source API lets any two grids on a page exchange rows by default, and enabling that implicitly is the accident Lattice's opt-in design exists to prevent. |
| rowTransfer | Passed through. There is no source-API property to translate it from, so a pair of grids that exchange rows names it explicitly. |
| Column key | Becomes |
|---|---|
| id | The column id, and its field. |
| header | The title. A multi-row header collapses to its first row, and says so: Lattice titles are a single string. |
| type | The data type, by name. |
| width, minWidth, maxWidth | layout.width, layout.min, layout.max. |
| resizable | layout.resizable. |
| hidden | layout.hidden. |
| draggable | layout.movable. |
| align | cell.align. |
| tooltip, tooltipTemplate | cell.tooltip. |
| template | cell.render. The source API returns HTML and Lattice mutates an element, so the return is treated as markup only when htmlEnable is set. |
| htmlEnable | Whether template output is trusted as markup. |
| sortable | sort.enabled. |
| editable | edit.enabled. |
| editorType | edit.editor, by name. An unrecognised one warns and falls back to the text editor. |
| editorConfig | edit.props. |
| options | edit.props.options, for a select-shaped editor. |
| summary | The column's total. |
| Config key | Becomes |
|---|---|
| dragItem: 'row' | rowReorder: true: same-grid drag-to-reorder. |
| rowTransfer | Passed straight through, unlike every other key: the source API 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. A <th data-edit> makes that column editable, data-type/data-format set its type/format, and a <tr data-lattice-row="…"> names the row key an out-of-band update later targets. <th data-width> (pixels or a percentage), data-min-width, data-max-width and data-flex size a hydrated column - the same width/minWidth/maxWidth/flex keys are accepted for hydrated columns in data-lattice-config - and with none given the columns flex to fill the host, wider for longer text. |
| serialiseState(grid, opts?) / restoreState(grid, encoded, opts?) | A compact, URL-safe encoding of everything grid.state covers (sort, filters, column order and widths, selection) diffed against the grid's own defaults first, so an untouched grid encodes to a handful of characters. Scroll position is included or restored only with { scroll: true }; the htmx module's history mechanism passes it at both ends, so a browser back/forward still lands where the user scrolled to. |
| <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, calendars from [data-lattice-calendar], Gantts from [data-lattice-gantt], maps from [data-lattice-map], 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. |
| autoInitGantt(root) / registerGantt(factory) | Builds a Gantt over every [data-lattice-gantt] element's server-rendered <table> - one <tr> per task, one <th data-field> per field. The factory is not inlined into the htmx bundle: register your own Gantt module with registerGantt(createGantt), or load it as a script tag exposing globalThis.LatticeGridGantt.createGantt, so a page with htmx and no Gantt pays nothing. Config comes from the same sibling data-lattice-config script/attribute the grid reads, with a nested split object for the split view. |
| driveGanttOobUpdates(opts?) | Applies an out-of-band [data-lattice-row="<task id>"] fragment to that task through gantt.rows.apply, so htmx never writes raw HTML into the rendered plan - one reschedule, zoom/scroll/selection/collapse untouched. |
| GANTT_CHANGE_EVENT | Every committed change (move, resize, progress, cell edit, link, add, delete, row move) re-raises a bubbling lattice:gantt-change on the host with detail { kind, id, task, changes } - start/end already converted to ISO dates - so hx-trigger="lattice:gantt-change" can POST it. See the hx-vals recipe. |
| GANTT_COMMIT_EVENT | One lattice:gantt-commit per user gesture or API transaction - a single bubbling event on the host carrying the whole edit in detail { cause, primary: { id, kind }, changes: [{ id, kind, task, changes }] } (primary the task the gesture acted on) - so hx-trigger="lattice:gantt-commit" posts a drag that pushed successors once instead of a burst of lattice:gantt-change events. See the hx-vals recipe. |
| autoInitDesigner(root) / registerDesigner(factory) | Builds a Designer over every [data-lattice-designer] element from its config - the mode, saved state and guardrails - with live data read from the grid named by its data-lattice-bind attribute or a grid: "<id>" config string (renamed by source: "<id>"). The factory is not inlined into the htmx bundle: register your own Designer module with registerDesigner(createDesigner), or load it as a script tag exposing globalThis.LatticeGridDesigner.createDesigner, so a page with htmx and no Designer pays nothing. Config comes from the same sibling data-lattice-config script/attribute the grid reads. |
| DESIGNER_CHANGE_EVENT | Every author change (add, remove, move, resize, page switch, undo, redo, panel chrome) re-raises a bubbling lattice:designer-change on the host with detail { state, cause } - the state includes the selected page - so hx-trigger="lattice:designer-change" can POST it. See the hx-vals recipe. |
| GRID_CHANGE_EVENT | Every committed cell edit on a declarative [data-lattice-grid] re-raises a bubbling lattice:grid-change on the host with detail { key, field, oldValue, newValue } - the row key, the edited column's field, and the old/new values - so hx-trigger="lattice:grid-change" can POST it. Nothing fires for a programmatic rows.apply/rows.load (those announce rows:changed, not cell:changed). See the hx-vals recipe. |
| CALENDAR_CHANGE_EVENT | Every move or resize on a declarative [data-lattice-calendar] re-raises a bubbling lattice:calendar-change on the host with detail { id, start, end } - the event id and the new start/end, already ISO strings - so hx-trigger="lattice:calendar-change" can POST it. See the hx-vals recipe. |
| autoInitMap(root) / registerMapFactories({ mapview, chart, leaflet, deck }) | Builds a map over every uninitialised [data-lattice-map] element, bound to a grid by data-lattice-bind/config.grid. The config's type selects the viewer: "mapview" (the default) builds the 3D Map View, "leaflet"/"deck" the matching adapter, anything else a chart type (markermap, geomap, …). No map engine is inlined into the htmx bundle: register your own factories with registerMapFactories, or load the modules as script tags exposing globalThis.LatticeGridMapView.createMapView/LatticeGrid.createChart/LatticeGridLeaflet.bindLeaflet/LatticeGridDeck.bindDeck, so a page with htmx and no map pays nothing. |
| destroyMapWithin(root) | Releases every map in a fragment before htmx detaches it - tile layers, observers and listeners - before the grid cleanup pass, so a map's own grid subscriptions are dropped by the viewer itself. |
| saveMapStateWithin / restoreMapStateWithin | Carry a map's view state (centre, zoom, selected layer) across htmx:beforeHistorySave/htmx:historyRestore in a data-lattice-map-state attribute, re-applied through the published wrapper's setState. |
| MAP_ATTR | The marker attribute autoInitMap scans for: data-lattice-map. |
| 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 - a Gantt's zoom, visible range, scroll, collapsed rows and column widths, a Designer's full state including the selected page, and a map's centre, zoom and selected layer. |
| autoInitCalendar(root) / registerCalendar(factory) | Builds a calendar over every [data-lattice-calendar] element, bound to a grid by data-lattice-bind/config.grid, or hydrated straight from a server-rendered <table> inside the element - bind wins, then table, then JSON rows. The calendar engine is inlined into the htmx bundle, so a page that declares a calendar needs no other module; registerCalendar(createCalendar) optionally overrides the factory. Config comes from the same sibling data-lattice-config script/attribute the grid reads. |
| destroyCalendarWithin / saveCalendarStateWithin / restoreCalendarStateWithin | Tear a calendar down before htmx detaches its fragment, and carry its view state (view, date, first day, time-axis scroll) across htmx:beforeHistorySave/htmx:historyRestore - the original table is swapped back in for the snapshot, then restoreSwappedCalendars() puts the rendered view back a microtask later. |
| driveCalendarOobUpdates(opts?) | Applies an out-of-band [data-lattice-row="<event id>"] fragment to that event through calendar.rows.apply, so htmx never writes raw HTML into the rendered view - one reschedule, the view, date and scroll untouched. Wired by attach(). |
| autoInitKanban(root) / registerKanban(factory) | Builds a Kanban board over every [data-lattice-kanban] element, bound to a grid by data-lattice-bind/config.grid, or hydrated straight from a server-rendered <table> inside the element - bind wins, then table, then JSON rows. The factory is not inlined into the htmx bundle: register your own Kanban module with registerKanban(createKanban), or load it as a script tag exposing globalThis.LatticeGridKanban.createKanban, so a page with htmx and no board pays nothing. |
| destroyKanbanWithin / saveKanbanStateWithin / restoreKanbanStateWithin | Tear a board down before htmx detaches its fragment, and carry its view state (collapsed columns and lanes, column order, quick filter, sprint/epic selection, selection) across htmx:beforeHistorySave/htmx:historyRestore - the original table is swapped back in for the snapshot, then restoreSwappedKanbans() puts the rendered board back a microtask later. |
| driveKanbanOobUpdates(opts?) | Applies an out-of-band [data-lattice-row="<card id>"] fragment to that card through board.rows.apply, so htmx never writes raw HTML into the rendered board - one move, collapsed columns and the sprint filter untouched. Wired by attach(). |
| KANBAN_CHANGE_EVENT | Every card move on a declarative [data-lattice-kanban] re-raises a bubbling lattice:kanban-change on the host - one event per moved card, with detail { id, from, to, index, lane } (the card key, the column it left, the column it landed in, the requested position and the swimlane) - so hx-trigger="lattice:kanban-change" can POST a drag. See the hx-vals recipe. |
| 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 - and a Gantt's zoom, visible range, scroll, collapsed rows and column widths. |
Declaring a Gantt, and posting its changes
A data-lattice-gantt element wraps the server-rendered task table - no JS needed to make the markup read as a table, and one htmx:load turns it into the split view. Because the Gantt engine is not inlined, the page loads the Gantt module itself, which publishes globalThis.LatticeGridGantt.createGantt that the adapter resolves. The lattice:gantt-change event re-raised for every edit carries the task id and the field-level changes (dates already ISO), so an hx-trigger posts exactly what changed.
<!-- The table names the task table's columns; the rows are the tasks. -->
<div data-lattice-gantt hx-trigger="lattice:gantt-change from:body"
hx-post="/tasks/{id}" hx-vals='js:{ id: event.detail.id, kind: event.detail.kind, changes: JSON.stringify(event.detail.changes) }'>
<table>
<thead><tr><th data-field="name">Task</th><th data-field="start">Start</th><th data-field="end">End</th></tr></thead>
<tbody>
<tr><td>Design</td><td>2026-09-28</td><td>2026-10-02</td></tr>
<tr><td>Build</td><td>2026-10-03</td><td>2026-10-09</td></tr>
</tbody>
</table>
</div>
<!-- htmx, then the Gantt module (publishes the factory the adapter resolves). -->
<script src="/dist/modules/gantt.esm.min.js"></script>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Posting a whole Gantt gesture at once
Dragging one bar reschedules its successors and summaries, so lattice:gantt-change fires once per task touched - a burst htmx cannot post as one synchronous request (each queued request loses its event, so js: hx-vals come out empty). For that, the adapter also re-raises the controller's one-per-action batch as a single bubbling lattice:gantt-commit: detail { cause, primary: { id, kind }, changes: [{ id, kind, task, changes }] }, where primary is the task the user dragged and changes lists every task the gesture moved. Point hx-trigger at it and post the whole edit once.
<!-- The table names the task table's columns; the rows are the tasks. -->
<div data-lattice-gantt hx-trigger="lattice:gantt-commit from:body"
hx-post="/tasks/commit"
hx-vals='js:{ cause: event.detail.cause, id: event.detail.primary.id, changes: JSON.stringify(event.detail.changes) }'>
<table>
<thead><tr><th data-field="name">Task</th><th data-field="start">Start</th><th data-field="end">End</th></tr></thead>
<tbody>
<tr><td>Design</td><td>2026-09-28</td><td>2026-10-02</td></tr>
<tr><td>Build</td><td>2026-10-03</td><td>2026-10-09</td></tr>
</tbody>
</table>
</div>
<!-- The plan's dependencies (auto-scheduled, so a drag pushes successors). -->
<script type="application/json" data-lattice-config>{
"projectStart": "2026-09-28",
"autoSchedule": true,
"dependencies": [{ "from": "a", "to": "b", "type": "FS" }],
"split": { "zoom": "day" }
}</script>
<!-- htmx, then the Gantt module (publishes the factory the adapter resolves). -->
<script src="/dist/modules/gantt.esm.min.js"></script>
Declaring a Designer, and posting its changes
A data-lattice-designer element builds a dashboard from its config - the mode, saved state and guardrails - with live data bound from a grid by data-lattice-bind (or grid/source in the config). Because the Designer engine is not inlined, the page loads the Designer module itself, which publishes globalThis.LatticeGridDesigner.createDesigner that the adapter resolves. The lattice:designer-change event re-raised for every author edit carries the whole state (including the selected page), so an hx-trigger posts exactly what changed.
<!-- The grid the designer reads; swapped in and out by htmx like any other. -->
<div id="sales" data-lattice-grid></div>
<script type="application/json" data-lattice-config>{"columns":[{"field":"region"},{"field":"sales","type":"number"}],"rows":[{"id":"a","region":"north","sales":10},{"id":"b","region":"south","sales":20}],"rowKey":"id"}</script>
<!-- The designer: mode, guardrails and saved state in the config; data bound to the grid. -->
<div data-lattice-designer data-lattice-bind="sales"
hx-trigger="lattice:designer-change from:body"
hx-post="/designers/main" hx-vals='js:{ state: JSON.stringify(event.detail.state), cause: event.detail.cause }'></div>
<script type="application/json" data-lattice-config>{
"mode": "edit",
"guardrails": { "maxPanels": 6 },
"state": {
"pages": [{ "id": "p1", "title": "Sales", "spec": { "layout": { "columns": 2 }, "panels": [
{ "id": "table", "kind": "grid", "title": "Orders", "source": "sales" }
] } }],
"selectedPageId": "p1",
"panels": {}
}
}</script>
<!-- htmx, then the Designer module (publishes the factory the adapter resolves). -->
<script src="/dist/modules/designer.esm.min.js"></script>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Posting a declarative grid's edits
A data-lattice-grid element is the grid's own declarative host: autoInit builds it, and GRID_CHANGE_EVENT re-raises each committed cell edit as a bubbling lattice:grid-change on that host. detail carries the row key, the edited column's field and the oldValue/newValue, so an hx-trigger posts exactly what changed. A programmatic rows.apply/rows.load posts nothing - it announces rows:changed on the grid's own bus, not cell:changed.
<!-- The grid, built declaratively; hx-trigger posts each committed edit. -->
<div data-lattice-grid hx-trigger="lattice:grid-change from:body"
hx-post="/rows/{key}" hx-vals='js:{ key: event.detail.key, field: event.detail.field, oldValue: event.detail.oldValue, newValue: event.detail.newValue }'>
<script type="application/json" data-lattice-config>{
"columns": [{ "field": "name" }, { "field": "status" }],
"rows": [{ "id": 1, "name": "Design", "status": "open" }],
"rowKey": "id"
}</script>
</div>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
A server table hydrating into an editable, postable grid
A server-rendered <table data-lattice-grid> hydrates straight into a grid with no config script: the <th> cells name the columns (data-field, data-type, data-format honoured as above), and <th data-edit> makes a column editable. A <tr data-lattice-row="…"> names the row key the out-of-band handler reads, so a later hx-swap-oob fragment can patch that row. Committed edits re-raise the same bubbling lattice:grid-change a config-built grid raises.
Columns are sized from the table too: <th data-width> fixes a width in pixels (or a percentage string), data-min-width/data-max-width bound it, and data-flex gives it a flex share of the leftover - the same width/minWidth/maxWidth/flex keys work on hydrated columns in the sibling data-lattice-config. With none given, hydrated columns fill the host width instead of the fixed 150px default: each column flexes in proportion to its longest text, with a sensible minimum, so a long heading is wider than a short one. Text that still does not fit ends in an ellipsis with its full value in the heading's title/accessible name, exactly as a declared grid.
<!-- The table is the whole grid: editable columns, row keys, edits posted back. -->
<table id="postable" data-lattice-grid hx-trigger="lattice:grid-change from:body"
hx-post="/rows" hx-vals='js:{ key: event.detail.key, field: event.detail.field, newValue: event.detail.newValue }'>
<thead><tr>
<th data-field="id" data-width="90">ID</th>
<th data-field="title" data-flex="3" data-edit>Title</th>
<th data-field="status" data-edit>Status</th>
</tr></thead>
<tbody>
<tr data-lattice-row="A1"><td>A1</td><td>Fix login</td><td>Todo</td></tr>
<tr data-lattice-row="A2"><td>A2</td><td>Write docs</td><td>Todo</td></tr>
</tbody>
</table>
<!-- The anchor next to the table is a sibling of the host after hydration; an
hx-swap-oob fragment for it patches that row in place. -->
<i id="postable-A2" data-lattice-row="A2"></i>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Posting a declarative calendar's moves
A data-lattice-calendar element, bound to a grid by data-lattice-bind or a grid string, re-raises each move or resize as a bubbling lattice:calendar-change on the host - detail { id, start, end } with the event id and the new start/end already ISO strings - so an hx-trigger posts the reschedule.
<!-- The grid the calendar follows, then the calendar itself. -->
<div id="cal-source" data-lattice-grid></div>
<script type="application/json" data-lattice-config>{"columns":[{"field":"start"}],"rows":[{"id":"a","start":"2026-09-28 10:00"}],"rowKey":"id"}</script>
<div data-lattice-calendar data-lattice-bind="cal-source" hx-trigger="lattice:calendar-change from:body"
hx-post="/events/{id}" hx-vals='js:{ id: event.detail.id, start: event.detail.start, end: event.detail.end }'></div>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Declaring a calendar view of a data grid
A server-rendered <table id="…" data-lattice-grid> hydrates into a grid whose host keeps the table's id, so a data-lattice-calendar beside it can bind by that id and draw one card per row. The calendar engine is inlined into the htmx bundle, so importing that one module builds the calendar - registerCalendar(createCalendar) is an optional override for an adapter that wants to swap in its own factory.
<!-- The server-rendered table: its id is the grid the calendar binds to. -->
<table id="cal-grid" data-lattice-grid>
<thead><tr><th data-field="title">Title</th><th data-field="start">Start</th></tr></thead>
<tbody>
<tr><td>Design</td><td>2026-09-28 10:00</td></tr>
<tr><td>Build</td><td>2026-09-29 11:00</td></tr>
<tr><td>Ship</td><td>2026-09-30 12:00</td></tr>
</tbody>
</table>
<!-- The calendar, bound to that grid by id: one card per row, on its start day. -->
<div data-lattice-calendar data-lattice-bind="cal-grid" style="width:720px;height:400px"></div>
<script type="application/json" data-lattice-config>{"startProperty":"start","view":"month","date":"2026-09-30"}</script>
<!-- htmx builds the calendar: its engine is inlined into the htmx module. -->
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
No grid needed: a calendar hydrates straight from the server-rendered <table> inside it. The <th data-field> cells name the fields, each <tr> is one event, and data-type/data-format are honoured exactly as the grid's own table hydration honours them. data-lattice-row on a <tr> names the event id the calendar keys by (else the config's rowKey field, else data-id, else the row index); an out-of-band <tr data-lattice-row="…"> later patches that one event in place. Keep an empty out-of-band target beside the table - an element carrying the event's id and data-lattice-row, like the <i> below - and the calendar preserves it across renders, so htmx always has somewhere to land the swapped row. The anchor needs no styling: the calendar keeps it out of layout itself, whatever markup the page uses.
<!-- The table is the data: the calendar replaces it, no grid in between. -->
<div data-lattice-calendar style="width:720px;height:400px">
<table>
<thead><tr><th data-field="title">Title</th><th data-field="start" data-type="datetime">Start</th></tr></thead>
<tbody>
<tr data-lattice-row="d"><td>Design</td><td>2026-09-28 10:00</td></tr>
<tr data-lattice-row="b"><td>Build</td><td>2026-09-29 11:00</td></tr>
<tr data-lattice-row="s"><td>Ship</td><td>2026-09-30 12:00</td></tr>
</tbody>
</table>
<!-- The out-of-band target htmx swaps into for a rescheduled event. -->
<i id="shift-d" data-lattice-row="d"></i>
</div>
<script type="application/json" data-lattice-config>{"startProperty":"start","view":"week"}</script>
<!-- htmx builds the calendar: its engine is inlined into the htmx module. -->
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Declaring a Kanban board
A data-lattice-kanban element carries the board's config in the same sibling data-lattice-config script (or attribute) the grid reads, and names the grid it follows by data-lattice-bind or a grid: "<id>" string - or hydrates straight from a server-rendered table inside it (below). Because the board engine is not inlined, the page loads the Kanban module itself, which publishes globalThis.LatticeGridKanban.createKanban that the adapter resolves. A board left on the page across a swap is re-pointed at the replacement grid through its own rebind(grid), and its view state (collapsed columns and lanes, the sprint filter, the search) rides browser history.
<!-- The grid the board follows, named by data-lattice-bind below. -->
<div id="work" data-lattice-grid><script type="application/json" data-lattice-config>{…}</script></div>
<!-- The board: config carries rows/columns/card; the binding names the grid. -->
<div data-lattice-kanban data-lattice-bind="work">
<script type="application/json" data-lattice-config>{
"columnProperty": "status",
"columns": [{ "id": "todo", "title": "To do" }, { "id": "done", "title": "Done" }],
"card": { "title": "title", "subtitle": "assignee" }
}</script>
</div>
<!-- htmx, then the Kanban module (publishes the factory the adapter resolves). -->
<script src="/dist/modules/kanban.esm.min.js"></script>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
No grid needed either: a board hydrates straight from a server-rendered <table> inside it - the <th data-field> cells name the fields, each <tr> is one card, and the board replaces the table. data-lattice-row on a <tr> names the card id the board keys by (else the config's rowKey field, else data-id, else the row index); an out-of-band <tr data-lattice-row="…"> later patches that one card in place. Keep an empty out-of-band target beside the table - an element carrying the card's id and data-lattice-row - and the board preserves it across renders, so htmx always has somewhere to land the swapped row. The anchor needs no styling: the board keeps it out of layout itself, whatever markup the page uses.
<!-- The table is the data: the board replaces it, no grid in between. -->
<div data-lattice-kanban style="width:900px;height:420px">
<table>
<thead><tr><th data-field="title">Title</th><th data-field="status">Status</th></tr></thead>
<tbody>
<tr data-lattice-row="a"><td>Alpha</td><td>todo</td></tr>
<tr data-lattice-row="b"><td>Beta</td><td>doing</td></tr>
<tr data-lattice-row="c"><td>Gamma</td><td>done</td></tr>
</tbody>
</table>
</div>
<script type="application/json" data-lattice-config>{"columnProperty":"status","columns":[{"id":"todo","title":"To do"},{"id":"doing","title":"Doing"},{"id":"done","title":"Done"}],"card":{"title":"title"}}</script>
<!-- htmx, then the Kanban module (publishes the factory the adapter resolves). -->
<script src="/dist/modules/kanban.esm.min.js"></script>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Posting a card move
A card drag re-raises a bubbling lattice:kanban-change on the host - one event per moved card, with detail { id, from, to, index, lane } - so an hx-trigger posts exactly the move the user made.
<!-- The board: a drag posts the card key, its old column and its new column. -->
<div data-lattice-kanban hx-trigger="lattice:kanban-change from:body"
hx-post="/cards/{id}" hx-vals='js:{ id: event.detail.id, from: event.detail.from, to: event.detail.to }'>
<table>
<thead><tr><th data-field="title">Title</th><th data-field="status">Status</th></tr></thead>
<tbody>
<tr data-lattice-row="a"><td>Alpha</td><td>todo</td></tr>
<tr data-lattice-row="b"><td>Beta</td><td>doing</td></tr>
</tbody>
</table>
</div>
<script type="application/json" data-lattice-config>{"columnProperty":"status","columns":[{"id":"todo","title":"To do"},{"id":"doing","title":"Doing"},{"id":"done","title":"Done"}],"card":{"title":"title"}}</script>
<!-- htmx, then the Kanban module (publishes the factory the adapter resolves). -->
<script src="/dist/modules/kanban.esm.min.js"></script>
<script type="module">import "/dist/modules/htmx.esm.min.js";</script>
Declaring a map, bound to its grid
A data-lattice-map element binds to a grid by id and draws one marker per row - no JS needed to make the markup read as a map, and one htmx:load builds it. The config's type selects the viewer; a chart type like markermap needs no CDN or WebGL, the "mapview" kind carries a view (centre and zoom) and a layer through htmx's history cache. Because the engines are not inlined, the page loads the one it uses itself, and registerMapFactories hands it to the adapter - the same seam registerGantt uses.
<!-- The grid the map follows, named by id. -->
<div id="g" data-lattice-grid></div>
<script type="application/json" data-lattice-config>{"columns":[{"field":"site"},{"field":"lng","type":"number"},{"field":"lat","type":"number"}],"rows":[{"id":"ldn","site":"London","lng":-0.13,"lat":51.5}],"rowKey":"id"}</script>
<!-- The map, bound to that grid; `type` names a chart type or the map view. -->
<div data-lattice-map data-lattice-bind="g" style="width:520px;height:360px"></div>
<script type="application/json" data-lattice-config>{"type":"markermap","lon":"lng","lat":"lat","label":"site"}</script>
<!-- htmx, then the chart module (register its factory for the adapter). -->
<script type="importmap">{ "imports": { "@toclocoinc/lattice-grid/modules/charts": "/dist/modules/charts.esm.js" } }</script>
<script src="/test/fixtures/htmx/htmx.min.js"></script>
<script type="module">
import { registerMapFactories } from "/dist/modules/htmx.esm.js";
import { createChart } from "/dist/modules/charts.esm.js";
import "/dist/modules/chart-markermap.esm.js";
registerMapFactories({ chart: createChart });
</script>
Type reference
Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.
Framework adapters
The component wrappers each framework package exports.
LatticeGridHandle
The live instance a `<LatticeGrid>` ref exposes; `null` before mount.
Property Type Description
grid Grid | null The live grid the component built, or null before the mount effect has run and after it has been destroyed. (read-only)
LatticeViewerHandle
The live instance a viewer component's ref exposes; `null` before mount.
Property Type Description
instance Instance | null The live viewer the component built - the board, panel, chart, plan, layout or strip - or null before mount. (read-only)
LatticeViewerCommonProps
What every viewer component takes beyond its own configuration: the grid it binds to, which published grid to take when that is left off, the lifecycle callbacks, and the host-element props.
Properties
Property Type Description
grid Grid | null The grid this viewer is built against; taken from context when absent. (optional)
gridName string Which published grid to take from context; `'default'` when absent. (optional)
className string Applied to the host element rather than to the viewer. (optional)
style Record<string, unknown> Applied to the host element rather than to the viewer. (optional)
id string Applied to the host element rather than to the viewer. (optional)
Methods
Method Signature Parameters Returns Description
onReady (instance: Instance) => void instance: Instance=> voidTold when the viewer exists. (optional)
onDestroy () => void - => voidTold just before it is destroyed. (optional)
Events
No events.
LatticeTabSpec
One tab of a `<LatticeTabs>`; `content` makes it React's rather than the module's.
Property Type Description
id string The tab's identity, used to select it, to name its slot and to derive another tab from it. Required, non-empty and unique; a duplicate or missing id is refused.
label string The text on the tab button. Defaults to the id. (optional)
content unknown | (() => unknown) A React element, or a function returning one, rendered through a portal. (optional)
[key: string] unknown Any other prop the host wants to carry on the tab. Passed through untouched, so a React host can key its own state off the same object the grid holds.
LatticeFormProps
The form component's callback props: one per form event.
Properties
No properties.
Methods
Method Signature Parameters Returns Description
onBeforeChange (event: FormBeforeChangeEvent) => unknown event: FormBeforeChangeEvent=> unknownRaised before a value commits; return `false` or a promise for `false` to veto. (optional)
onChange (event: FormValueEvent) => void event: FormValueEvent=> voidA value committed. (optional)
onInput (event: FormValueEvent) => void event: FormValueEvent=> voidA live, uncommitted value. (optional)
onClick (event: FormClickEvent) => void event: FormClickEvent=> voidA button item was pressed. (optional)
onFocus (event: FormFocusEvent) => void event: FormFocusEvent=> voidAn item's control gained focus. (optional)
onBlur (event: FormFocusEvent) => void event: FormFocusEvent=> voidAn item's control lost focus. (optional)
onValidated (event: FormValidatedEvent) => void event: FormValidatedEvent=> void`validate()` finished, with every failure. (optional)
Events
No events.
LatticeDataGridColDef
One column definition, as `<LatticeDataGrid>` takes it. These eleven keys are mapped onto a grid column; any other key is refused once, by name, as `columns.<key>`.
Property Type Description
field string The row property the column shows; also the column's id. Required.
headerName string The heading text. Defaults to the field. (optional)
width number The width in pixels; 100 by default, as in MUI. Ignored when `flex` is set. (optional)
flex number A share of the spare width, instead of a fixed `width`. (optional)
type 'string' | 'number' | 'date' | 'dateTime' | 'boolean' The value type: `'string'`, `'number'`, `'date'`, `'dateTime'` or `'boolean'`, each mapped to the grid's own type. Any other type is refused by name and the type is inferred from the data. (optional)
editable boolean Let the reader edit this column's cells. (optional)
sortable boolean `false` stops the column being sorted. (optional)
filterable boolean `false` stops the column being filtered. (optional)
Method Signature Parameters Returns Description
valueGetter (value: any, row: any, column: LatticeDataGridColDef, apiRef: { current: LatticeDataGridApi | null }) => unknown value: any
row: any
column: LatticeDataGridColDef
apiRef: { current: LatticeDataGridApi | null }=> unknownDerive the cell value, MUI's four-argument form: the row's value for `field`, the row, this definition, and the `apiRef`. (optional)
valueFormatter (value: any, row: any, column: LatticeDataGridColDef, apiRef: { current: LatticeDataGridApi | null }) => unknown value: any
row: any
column: LatticeDataGridColDef
apiRef: { current: LatticeDataGridApi | null }=> unknownTurn the cell value into the text the cell shows, with the same four arguments. (optional)
renderCell (params: LatticeDataGridCellParams) => unknown params: LatticeDataGridCellParams=> unknownRender the cell: a string or number is shown as text, a React element is mounted into the cell through its own root (which needs `ReactDOM` passed to `createLatticeDataGrid`). (optional)
LatticeDataGridCellParams
What `onCellClick`, `onCellDoubleClick`, `onCellEditStop` and `renderCell` receive, MUI's shape.
Property Type Description
id unknown The row's MUI id: `getRowId(row)`, or `row.id`.
field string The column's field.
value unknown The cell's value.
formattedValue unknown The cell's display text.
row any The row object.
colDef LatticeDataGridColDef | undefined The column's definition, or undefined for a column the grid generated.
cellMode 'view' Always `'view'`: the callbacks fire outside an edit.
hasFocus boolean Always false; focus is not reported.
tabIndex number Always -1.
api LatticeDataGridApi The `apiRef` methods, on `renderCell` only. (optional)
LatticeDataGridRowParams
What `onRowClick` and `onRowEditStop` receive, MUI's shape.
Property Type Description
id unknown The row's MUI id.
row any The row object.
columns LatticeDataGridColDef[] Every column definition.
field string The field the edit stopped on, on `onRowEditStop` only. (optional)
LatticeDataGridCallbackDetails
The second argument MUI's `on*Change` callbacks receive.
Property Type Description
api LatticeDataGridApi The `apiRef` methods.
LatticeDataGridSortItem
One sort item, MUI's shape.
Property Type Description
field string The column field.
sort 'asc' | 'desc' | null | undefined The direction; an item with no direction is dropped.
LatticeDataGridFilterItem
One filter item, MUI's shape.
Property Type Description
id number | string An id for the item; kept for the host, not read. (optional)
field string The column field.
operator string The MUI operator. Every free-tier operator translates; an operator that does not makes the whole model refused, never partly applied.
value unknown The operand. An item with no operand is skipped, except `isEmpty`/`isNotEmpty`. (optional)
LatticeDataGridFilterModel
The filter model, MUI's shape.
Property Type Description
items LatticeDataGridFilterItem[] The conditions.
logicOperator 'and' | 'or' How the items combine; `'and'` by default. (optional)
quickFilterValues unknown[] Quick-filter words: every word must match somewhere in the row. (optional)
quickFilterLogicOperator 'and' | 'or' `'and'` only: `'or'` has no quick-filter equivalent and refuses the model. (optional)
LatticeDataGridRowSelectionModel
The row-selection model, MUI's shape. `'exclude'` is refused by name.
Property Type Description
type 'include' | 'exclude' `'include'`: exactly these rows are selected.
ids Set<unknown> The selected rows' MUI ids.
LatticeDataGridPaginationModel
The page model, MUI's shape; the page is zero-based.
Property Type Description
page number The zero-based page.
pageSize number Rows per page.
LatticeDataGridApi
The methods the `apiRef` exposes: the commonly used six, with MUI's signatures. `apiRef.current` is set when the grid is built and cleared when it is destroyed.
Properties
No properties.
Methods
Method Signature Parameters Returns Description
getSelectedRows (): Map<unknown, any> - Map<unknown, any>The selected rows, keyed by MUI row id.
setSortModel (model: LatticeDataGridSortItem[]): void model: LatticeDataGridSortItem[]voidReplace the sort model. `onSortModelChange` fires, as in MUI.
setFilterModel (model: LatticeDataGridFilterModel): void model: LatticeDataGridFilterModelvoidReplace the filter model. `onFilterModelChange` fires once, as in MUI.
scrollToIndexes (params: { rowIndex?: number; colIndex?: number }): boolean params: { rowIndex?: number; colIndex?: number }booleanScroll a row, a column, or a cell into view. The column index counts the checkbox column as 0 when there is one, as MUI's does. Returns false when neither index names anything.
exportDataAsCsv (options?: Record<string, unknown>): void options?: Record<string, unknown>voidDownload the rows as CSV: the selected rows when there are any, otherwise every row the filters pass. `fileName`, `delimiter`, `includeHeaders`, `utf8WithBom`, `fields`, `allColumns` and `escapeFormulas` are mapped; any other option is refused by name.
updateRows (updates: Array<Record<string, unknown>>): void updates: Array<Record<string, unknown>>voidMerge a partial row into the row with its id, add a row with a new id, or delete one with `_action: 'delete'`. `_action: 'replace'` is refused by name.
Events
No events.
LatticeDataGridProps
What `<LatticeDataGrid>` takes: the DataGrid props it maps, and nothing else. Every other prop is refused once, by name, as `react:mui-prop-unsupported:<prop>`, and ignored.
Properties
Property Type Description
rows readonly any[] The rows, for a memory source. Each needs an `id`, unless `getRowId` says otherwise. (optional)
columns readonly LatticeDataGridColDef[] The column definitions. Hoist or memoise them: a new array rebuilds the columns.
loading boolean Show the loading overlay while true. (optional)
density 'compact' | 'standard' | 'comfortable' Row density: `'compact'`, `'standard'` or `'comfortable'`. (optional)
checkboxSelection boolean A checkbox column, with a select-all box in its heading, and multiple selection. (optional)
disableRowSelectionOnClick boolean A click on a row does not select it; only the checkbox does. (optional)
editMode 'cell' | 'row' `'cell'` or `'row'` editing; `'row'` is what makes `onRowEditStop` fire. (optional)
sortModel LatticeDataGridSortItem[] The sort, applied when its identity changes. (optional)
filterModel LatticeDataGridFilterModel The filter, applied when its identity changes. (optional)
rowSelectionModel LatticeDataGridRowSelectionModel | readonly unknown[] The selected rows, applied when its identity changes. An array of ids is taken too. (optional)
columnVisibilityModel Record<string, boolean> `{ [field]: false }` hides a column; applied when its identity changes. (optional)
paginationModel LatticeDataGridPaginationModel The page and page size, client-side over a memory source only. Over any other source it is refused by name and the rows are left untouched. (optional)
pageSizeOptions ReadonlyArray<number | { value: number; label: string }> The page sizes the pager offers; `[25, 50, 100]` by default. Memory source only. (optional)
apiRef { current: LatticeDataGridApi | null } An object whose `current` is set to the {@link LatticeDataGridApi} while the grid exists. (optional)
slots { toolbar?: unknown } Component slots; only `toolbar` is mapped, and it renders when `showToolbar` is true. (optional)
showToolbar boolean Render `slots.toolbar` above the grid. (optional)
source SourceConfig | Record<string, unknown> Not an MUI prop: a Lattice `source` for rows that are not in memory. Over one, the three paging props are refused by name. (optional)
className string Applied to the host element. (optional)
style Record<string, unknown> Applied to the host element, over its flex-column default. (optional)
Methods
Method Signature Parameters Returns Description
getRowId (row: any) => string | number row: any=> string | numberRead a row's id, when it is not `row.id`. Read at call time, so an inline arrow is fine. (optional)
onSortModelChange (model: LatticeDataGridSortItem[], details: LatticeDataGridCallbackDetails) => void model: LatticeDataGridSortItem[]
details: LatticeDataGridCallbackDetails=> voidTold when the reader or `apiRef` changes the sort; never for the prop's own change. (optional)
onFilterModelChange (model: LatticeDataGridFilterModel, details: LatticeDataGridCallbackDetails) => void model: LatticeDataGridFilterModel
details: LatticeDataGridCallbackDetails=> voidTold when the reader or `apiRef` changes the filter. (optional)
onRowSelectionModelChange (model: LatticeDataGridRowSelectionModel, details: LatticeDataGridCallbackDetails) => void model: LatticeDataGridRowSelectionModel
details: LatticeDataGridCallbackDetails=> voidTold when the selection changes, always with an `'include'` model. (optional)
onColumnVisibilityModelChange (model: Record<string, boolean>, details: LatticeDataGridCallbackDetails) => void model: Record<string, boolean>
details: LatticeDataGridCallbackDetails=> voidTold when a column is shown or hidden, with every column's visibility. (optional)
onPaginationModelChange (model: LatticeDataGridPaginationModel, details: LatticeDataGridCallbackDetails) => void model: LatticeDataGridPaginationModel
details: LatticeDataGridCallbackDetails=> voidTold when the page or page size changes; memory source only. (optional)
onRowClick (params: LatticeDataGridRowParams, event: unknown, details: LatticeDataGridCallbackDetails) => void params: LatticeDataGridRowParams
event: unknown
details: LatticeDataGridCallbackDetails=> voidTold when a row is clicked. (optional)
onCellClick (params: LatticeDataGridCellParams, event: unknown, details: LatticeDataGridCallbackDetails) => void params: LatticeDataGridCellParams
event: unknown
details: LatticeDataGridCallbackDetails=> voidTold when a cell is clicked. (optional)
onCellDoubleClick (params: LatticeDataGridCellParams, event: unknown, details: LatticeDataGridCallbackDetails) => void params: LatticeDataGridCellParams
event: unknown
details: LatticeDataGridCallbackDetails=> voidTold when a cell is double-clicked. (optional)
onCellEditStop (params: LatticeDataGridCellParams, event: { defaultMuiPrevented: boolean }, details: LatticeDataGridCallbackDetails) => void params: LatticeDataGridCellParams
event: { defaultMuiPrevented: boolean }
details: LatticeDataGridCallbackDetails=> voidTold when a cell edit ends. The edit has ended; `defaultMuiPrevented` is not read. (optional)
onRowEditStop (params: LatticeDataGridRowParams, event: { defaultMuiPrevented: boolean }, details: LatticeDataGridCallbackDetails) => void params: LatticeDataGridRowParams
event: { defaultMuiPrevented: boolean }
details: LatticeDataGridCallbackDetails=> voidTold when a row edit ends, in `editMode="row"`. (optional)
onColumnResize (params: { colDef: LatticeDataGridColDef | undefined; width: number; element: null }, event: unknown, details: LatticeDataGridCallbackDetails) => void params: { colDef: LatticeDataGridColDef | undefined; width: number; element: null }
event: unknown
details: LatticeDataGridCallbackDetails=> voidTold when a column is resized. (optional)
onColumnOrderChange (params: { column: LatticeDataGridColDef | undefined; targetIndex: number; oldIndex: number }, event: unknown, details: LatticeDataGridCallbackDetails) => void params: { column: LatticeDataGridColDef | undefined; targetIndex: number; oldIndex: number }
event: unknown
details: LatticeDataGridCallbackDetails=> voidTold when a column is moved. (optional)
onCellModesModelChange (model: Record<string, Record<string, { mode: 'edit' }>>, details: LatticeDataGridCallbackDetails) => void model: Record<string, Record<string, { mode: 'edit' }>>
details: LatticeDataGridCallbackDetails=> voidTold when a cell enters or leaves edit mode: `{ [id]: { [field]: { mode: 'edit' } } }`. (optional)
onRowModesModelChange (model: Record<string, { mode: 'edit' }>, details: LatticeDataGridCallbackDetails) => void model: Record<string, { mode: 'edit' }>
details: LatticeDataGridCallbackDetails=> voidTold when a row enters or leaves edit mode: `{ [id]: { mode: 'edit' } }`. (optional)
onDataSourceError (error: unknown) => void error: unknown=> voidTold when the grid's `source` reports an error. (optional)
Events
No events.
LatticeDesignerHandle
The live designer a `<LatticeDesigner>` ref exposes; `null` before mount.
Property Type Description
instance Designer | null The live designer the component built, or null before mount and after destroy. (read-only)
designer Designer | null The same designer, under its own name. (read-only)
LatticeVueGridExposed
What a `<LatticeGrid>` template ref exposes; `grid()` is null before mount.
Method Signature Parameters Returns Description
grid (): Grid | null - Grid | nullThe live grid the component built, or null before mount.
LatticeVueViewerExposed
What a viewer's template ref exposes; `instance()` is null before mount.
Method Signature Parameters Returns Description
instance (): Instance | null - Instance | nullThe live viewer the component built, or null before mount.
LatticeVueTabSpec
One tab of a `<LatticeTabs>`; a slot named for its `id` makes it Vue's.
Property Type Description
id string The tab's identity, which also names the slot whose content Vue renders into the panel. Required, non-empty and unique.
label string The text on the tab button. Defaults to the id. (optional)
[key: string] unknown Any other prop the host wants to carry on the tab. Passed through untouched, so a Vue host can key its own state off the same object the grid holds.
LatticeVueGridRegistry
Where grids publish themselves so the viewers around them can find one.
Properties
Property Type Description
grids { value: Readonly<Record<string, Grid>> } A shallow ref of name → grid; replaced, never mutated, on each change.
Methods
Method Signature Parameters Returns Description
publish (name: string, grid: Grid | null): void name: string
grid: Grid | nullvoidPublish a grid under a name, or withdraw it with `null`.
Events
No events.
LatticeVueRouterHandle
The Data Router handle a `provideLatticeRouter` puts in scope.
Properties
Property Type Description
router unknown The live router, built the first time anything asks for it. (read-only)
Methods
Method Signature Parameters Returns Description
attach (grid: Grid, route: unknown, opts?: Record<string, unknown>): void grid: Grid
route: unknown
opts?: Record<string, unknown>voidAttach a grid to a route.
detach (grid: Grid): void grid: GridvoidDetach a grid, before it is destroyed.
destroy (): void - voidDestroy the router; called for you when the providing scope is disposed.
Events
No events.
LatticeGridElement
`<lattice-grid>`. Structures are properties, scalars are the attributes in the table above, and `config` merges anything without an attribute of its own. Instance events arrive as `CustomEvent`s named `lattice-` plus the event name with colons hyphenated; `grid-ready` and `grid-destroyed` are this adapter's own and carry {@link LatticeGridEventDetail}.
Properties
Property Type Description
theme string | undefined Every attribute in the table above is also a property of the same name.
density string | undefined The row height and padding as a named step, rather than pixel by pixel. The `density` attribute.
locale string | undefined The locale the grid formats and sorts in. The `locale` attribute.
rowKey string | undefined Which field identifies a row. As an attribute it can only be a field name; the composite and function forms are properties.
rowHeight number | undefined Row height in pixels. The `row-height` attribute, parsed as a number.
headerHeight number | undefined Header height in pixels. The `header-height` attribute, parsed as a number; unset, the header follows the density token.
autoHeight boolean | undefined Let the grid grow to its rows rather than capping its height. The `auto-height` attribute is a bare boolean - present is true.
selection string | undefined What the reader may select: `single`, `multiple` or `none`. The `selection` attribute; the full selection config goes through `config`.
grid Grid | null The live grid, or null while the element is disconnected. (read-only)
instance Grid | null The live grid, under the name every viewer element uses. (read-only)
rows Row[] The row data.
columns Column[] The column definitions.
config GridConfig Any configuration without an attribute of its own; merged, not replaced.
predicates Record<string, ((row: Row) => boolean) | null> | undefined Named row predicates, registered through `grid.filters.where` - they compose with whatever filter the reader has set, unlike `filters`.
rowUpdates { add?: Row[]; update?: Row[]; remove?: unknown[] } | undefined A keyed diff applied straight to `grid.rows.apply()`.
Methods
No methods.
Events
No events.
LatticeViewerElement
What every viewer element carries.
Properties
Property Type Description
instance Instance | null The live viewer, or null before it is built. Every configuration key below is also a property, and an attribute of the same name is the same key. A key none of these interfaces declares works as a property too - the element adopts it - but is written through `config` where the declarations are the contract. (read-only)
grid unknown The grid this viewer is built against, when it is given one directly.
gridName string | undefined Which published grid to bind to; the `grid-name` attribute in markup.
config Record<string, unknown> Any configuration without a property of its own; merged, not replaced.
Methods
No methods.
Events
No events.
LatticeKPIElement
`<lattice-kpi>`. Grid-bound by default, and it **waits** for its grid rather than mounting empty; give it `rows` for a standalone panel.
Property Type Description
tiles unknown[] The panel's tiles. A structure, so it is a property rather than an attribute.
rows Row[] Rows for a standalone panel. Set them and the element stops waiting for a grid.
columns number | undefined How many tile columns to aim for.
rowKey string | undefined Which field identifies a row, for the panel's own keyed store.
locale string | undefined The default locale a clock tile formats in.
LatticeChartElement
`<lattice-chart>`. Always drawn from a grid, so it waits for one.
Property Type Description
shapes unknown A geomap's geometry: a loaded pack, `{ pack: id }`, GeoJSON, or a map of code to path data. A structure, so it is a property.
series unknown The column that splits the measure into one series per distinct value.
data unknown Rows to draw instead of the grid's own filtered rows - the chart spec's `rows`, under the name the element gives it. An explicit `config: { rows }` wins.
type string | undefined Which chart to draw. The `type` attribute.
x string | undefined The category column.
y string | string[] | undefined The measure column, or several for a multi-measure chart.
value string | undefined The measure a markermap writes beside each dot and colours it by.
label string | undefined The row-label column, for the types that name their rows.
lon string | undefined The longitude column, in degrees east, for the maps that place a row by where it is.
lat string | undefined The latitude column, in degrees north.
labels boolean | undefined Print the value beside each mark. The `labels` attribute is a bare boolean.
stacked boolean | undefined Stack the series rather than drawing them side by side - the spec's `stack`.
horizontal boolean | undefined Draw a `type="bar"` chart along y instead of x, by selecting the `horizontalBar` type. Ignored on any other chart type, whose orientation is the type's own.
scheme string | undefined The colour scheme by name.
LatticeKanbanElement
`<lattice-kanban>`. Grid-bound like the KPI panel; give it `rows` to stand alone.
Property Type Description
rows Row[] Rows for a standalone board. Set them and the element stops waiting for a grid.
columns unknown[] The board's column definitions.
card unknown The card template's field mapping - title, subtitle, labels, assignee and the rest.
rowKey string | undefined Which field identifies a card.
columnProperty string | undefined The row property that puts a card in a column.
titleProperty string | undefined The row property a card's headline comes from - the shorthand for `card: { title }`, which wins where both are given.
swimlanes boolean | undefined Draw the two-dimensional swimlane layout. The `swimlanes` attribute is a bare boolean; name the lane property through `swimlane-property`.
LatticeCalendarElement
`<lattice-calendar>`. Grid-bound like the board; give it `rows` to stand alone.
Property Type Description
rows Row[] Rows for a standalone calendar. Set them and the element stops waiting for a grid.
card unknown The default card template's field mapping - title, subtitle and the rest.
header unknown The header toolbar sections, or `false` to render none.
colorMap unknown An optional value→colour map for the `color-property` field.
labels unknown Host-localised labels.
renderCard unknown A host card renderer `(row, el)`.
rowKey string | undefined Which field identifies a card.
view 'week' | 'month' | undefined The view to show: `week` or `month`. The `view` attribute.
date unknown The anchor date the visible range centres on.
startProperty string | undefined The field naming a point event's start.
endProperty string | undefined The optional field naming a span's end.
allDayProperty string | undefined An optional boolean field marking an all-day event.
firstDay number | undefined Week-start weekday, 0=Sun…6=Sat.
locale string | undefined BCP-47 locale.
timeZone string | undefined IANA time-zone name.
colorProperty string | undefined The property whose value picks the card's edge colour.
eventOrder string | undefined Card stacking order within a day.
dayMaxEvents number | undefined The `+N more` overflow threshold per day.
readonly unknown Granular readonly (whole calendar, per day, per card).
ariaLabel string | undefined The calendar's accessible name.
Method Signature Parameters Returns Description
move (key: unknown, to: { start?: Date | null; end?: Date | null }): Promise<boolean> key: unknown
to: { start?: Date | null; end?: Date | null }Promise<boolean>Reschedule a card: move it to a new start (or resize its end), gated by `beforeMove` and announced by `card:move` (`lattice-beforeMove` / `lattice-card-move`). Resolves whether the reschedule stuck.
LatticeGanttElement
`<lattice-gantt>`.
Property Type Description
tasks unknown[] The plan's tasks. A structure, so it is a property.
dependencies unknown[] The links between tasks.
resources unknown The resource capacities, for over-allocation and levelling.
calendar unknown The working-time calendar: the `weekends` preset, or explicit workdays and holidays.
columns unknown The table panel's columns, for the split view.
rowKey string | undefined Which field identifies a task.
scale string | undefined The timeline's zoom - the shorthand for `config: { render: { zoom } }`, which wins where both are given.
autoSchedule boolean | undefined Cascade an edit down the dependency chain rather than only recomputing. A bare boolean attribute.
LatticeLayoutElement
`<lattice-layout>`.
Property Type Description
windows unknown[] The windows to place, each with its content and its cell. A structure, so it is a property.
columns number | undefined How many columns the grid of windows has.
rows number | undefined How many rows the grid of windows has.
compact string | undefined Which way windows collapse into the space a closed one left.
LatticeTabsElement
`<lattice-tabs>`. A tab's content is a `<template data-tab="‹id›">` child, cloned into the panel the module creates for it; a tab with no template is left to the module.
Property Type Description
tabs unknown[] The tab descriptors. A tab's content comes from a `<template data-tab="‹id›">` child when there is one.
LatticeRouterElement
`<lattice-router>`. Owns a Data Router for the life of the element, built by the first `<lattice-grid route="…">` beneath it.
Properties
Property Type Description
router unknown The live router, or null until something routes. (read-only)
instance unknown The live router, under the name every viewer element uses. (read-only)
config Record<string, unknown> The router configuration; read once, when the router is built.
Methods
No methods.
Events
No events.
StateApplyReport
Property Type Description
applied string[] The state keys that were restored, in application order.
skipped { key: string; reason: string }[] The keys that were not restored, each with a reason in words a host can show - `not an array`, `unknown column`, `skipped by caller`, and so on. A partial restore is reported, never thrown.