api reference
Framework adapters and custom elements
A component for every view in React, Vue, Svelte and Angular, a custom element per view, declarative init, the dhtmlx wrapper and htmx.
API reference › Framework adapters and custom elements
All 13 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. |
| 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. |
| 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. |
| 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.createLatticeGantt({ React, createGantt: stub }),
A.createLatticeLayout({ React, createLayout: stub }),
A.createLatticeTabs({ React, ReactDOM, createTabs: stub }),
A.createLatticeViewer({ React, viewer: 'kpi', mount: 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}`;
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.
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. |
| 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. |
| 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.createLatticeGantt({ vue, createGantt: stub }),
A.createLatticeLayout({ vue, createLayout: stub }),
A.createLatticeTabs({ vue, createTabs: 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. |
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. |
| 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. |
| 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. |
| 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. |
| 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-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'));
dhtmlx Grid compatibility wrapper
A Grid class shaped like dhtmlx's own dhx.Grid (Suite 5+), backed by a real Lattice grid. Covers column definitions, .data, .selection, .history, .export.csv/.xlsx, and a name-mapped subset of .events: see the guide for exactly what is and is not covered. Not the classic pre-Suite-5 dhtmlXGridObject.
import { Grid } from './dist/modules/dhtmlx-compat.esm.min.js';
const grid = new Grid(container, {
columns: [{ id: 'name', header: [{ text: 'Name' }], sortable: true }],
data: rows,
});
grid.data.serialize(); // every row's data, in source order
| Namespace | Covers |
|---|---|
| .data | add, update, remove, removeAll, parse, load, find, findAll, exists, getItem, getId, getIndex, getLength, forEach, serialize, sort, filter, resetFilter. Index means current display order throughout. |
| .selection | setCell(rowId, colId, ctrlUp?, shiftUp?), getCell, getCells, isSelectedCell, removeCell. row/column carry .id; a row's own fields sit alongside it, matching dhtmlx's own IRow. |
| .history | undo, redo, canUndo, canRedo, clear, getHistory. |
| .export | csv, xlsx. pdf/png throw, no raster export to translate to. |
| .events | cellClick/cellDblClick/cellRightClick/afterEditStart/afterEditEnd/afterSort call your handler with dhtmlx's own positional arguments. afterRowDrop fires (data, event) from a same-grid reorder settling or a row landing from another grid. Every other mapped name passes Lattice's own event object. Every before*/can*/cancel* event, and row/column drag negotiation (a handler refusing or steering a drop mid-gesture), is unmapped: logged once per name if subscribed to. |
| .rangeSelection | Best-effort only: its range shape is this wrapper's own design, not dhtmlx's genuine RangeSelection module. |
Which dhtmlx 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, as dhtmlx does. |
| 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: dhtmlx 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 dhtmlx 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. dhtmlx 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: dhtmlx allows any two dragItem: 'row' grids on a page to exchange rows by default; Lattice's rowTransfer is deliberately opt-in per pair, so there is nothing to derive it from. |
Declarative init, hydration & state
Core-level primitives, usable with or without any framework adapter or the htmx module below. An element built by createGrid is discoverable from itself: element.__lattice holds the live instance, cleared on destroy().
| Export | Does |
|---|---|
| autoInit(root) | Builds a grid on every [data-lattice-grid] element under root not already built: idempotent, safe to call again after new content arrives. Config comes from a sibling <script type="application/json" data-lattice-config>; without one, a <table> element is hydrated instead. |
| hydrateTable(table, config?) | Reads a <table>'s header row for column definitions and body rows for data (type-inferred per cell), then replaces the table with the grid. config (columns, rows, anything else) always wins over what was inferred. |
| serialiseState(grid) / restoreState(grid, encoded) | A compact, URL-safe encoding of everything grid.state covers (sort, filters, column order and widths, scroll position, selection) diffed against the grid's own defaults first, so an untouched grid encodes to a handful of characters. |
| <meta name="lattice-license" content="…"> | Read automatically when no licence is passed to createGrid, no imperative call required. |
htmx integration
One file. modules/htmx re-exports createGrid, autoInit, hydrateTable, readTable, serialiseState and restoreState alongside its own exports below, so a page using htmx integration never also loads the base bundle: that would mean two independent copies of the whole engine on one page. Registers on import: builds grids from [data-lattice-grid] elements or server-rendered <table>s, tears them down before htmx detaches a swapped-out subtree, and rebuilds them in newly-loaded content. driveServerMode/driveInfiniteScroll drive sort, filter and infinite scroll over plain htmx requests; driveOobUpdates applies out-of-band row updates in place. See the guide for the full request-lifecycle wiring and why infinite scroll uses two triggers.
import { autoInit, driveServerMode, driveInfiniteScroll } from './dist/modules/htmx.esm.min.js';
autoInit(document); // builds every [data-lattice-grid] under it
| Export | Does |
|---|---|
| createGrid, autoInit, hydrateTable, readTable, serialiseState, restoreState | The same functions documented above, re-exported: this is the one import a page using htmx integration needs for both grid construction and htmx wiring. |
| driveServerMode(grid, trigger, opts?) | A sort or filter change fires a request on trigger carrying offset/limit/sort/filters; the response replaces the grid's rows. opts.columns for the HTML-fragment ingest path. |
| driveInfiniteScroll(grid, sentinel, opts?) | Appends rows as the grid's own visible window nears the end of what's loaded. sentinel's own hx-trigger names revealed, lattice:scroll-near-end, the first fires the initial chunk, the second every chunk after. opts.threshold (default 20) sets how many rows from the end counts as near. |
| driveOobUpdates(grid, opts?) | Applies an out-of-band swap landing on [data-lattice-row="<key>"] to that row, in place: scroll, selection and filter state untouched. |
| Browser history | serialiseState/restoreState above, wired automatically to htmx:beforeHistorySave/htmx:historyRestore once this module is imported: browser back restores the prior sort, filter and scroll position. |
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 | => void | Told when the viewer exists. (optional) |
| onDestroy | () => void | - | => void | Told 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. |
LatticeVueGridExposed
What a `<LatticeGrid>` template ref exposes; `grid()` is null before mount.
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| grid | (): Grid | null | - | Grid | null | The 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 | null | The 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: stringgrid: Grid | null | void | Publish 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: Gridroute: unknownopts?: Record<string, unknown> | void | Attach a grid to a route. |
| detach | (grid: Grid): void | grid: Grid | void | Detach a grid, before it is destroyed. |
| destroy | (): void | - | void | Destroy 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`. |
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.