developer guide
Export, clipboard and the web component
Excel and CSV export written with no dependency, clipboard interop with Excel and Sheets, the <lattice-grid> web component, and the full event API.
Developer guide › Export, clipboard and the web component
Export
CSV, Excel, clipboard, print
grid.export.csv({ download: true, fileName: 'circuits' });
grid.export.excel({ download: true, sheetName: 'Circuits' });
grid.export.clipboard({ headers: true, rows: 'range' });
grid.export.print();
Exports follow what the user is looking at, the current filter, sort and column order, formatted values, and only the columns they may read.
On the server
A user asks to export half a million rows. You can ship them all to the browser to be formatted, or write the export server-side and watch it drift from what the grid shows. The headless core is the third option.
The same code, in Node
import { createHeadlessGrid } from '@toclocoinc/lattice-grid';
const grid = createHeadlessGrid({ columns, rows: fromDatabase });
grid.state.apply(savedView.state);
return grid.export.csv();
A saved view is a serialisable state object, so the server applies exactly what the user set up, using exactly the code the browser uses. The £1,234.50 in the file is the £1,234.50 on the screen because it came out of the same formatter. 50,000 rows filtered, sorted and written to CSV takes about 100ms.
Clipboard
Copy produces the tab-separated form Excel, Numbers and Sheets all read, so a range pastes as cells rather than as one lump of text. Values go through each column's clipboard hook: a lookup copies its label, a date copies an unambiguous form.
Copy and paste
grid.export.rangeText(); // the text, without writing it
grid.export.clipboard({ rows: 'range' }); // copy the range
grid.export.clipboard({ headers: true, rows: 'selected' });
grid.edit.pasteInto(text); // Excel's tiling rules
Pasting follows the spreadsheet convention: one cell into a range fills the range, one row into several rows repeats down, and a block larger than the target extends past it. People have twenty years of muscle memory for this and a grid that invents its own rules is a grid people fight.
Web component
<lattice-grid> is the grid as a custom element, shipped as a self-contained
module bundle. It exists for pages without a bundler, a Rails, Django or Laravel template
that wants a grid without adopting a front-end build.
The whole integration
<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"},{"id":"B","city":"Cardiff"}]'></lattice-grid>
Load this or the main bundle, not both. The module is self-contained: it
carries the grid with it, so a page that also loads lattice-grid.esm.js
downloads and evaluates the grid twice.
Driven from script
// Structures are properties; scalars are attributes.
const el = document.createElement('lattice-grid');
el.setAttribute('row-key', 'id');
el.columns = [{ field: 'id' }, { field: 'charge', type: 'number' }];
el.rows = data;
document.body.appendChild(el);
// The full imperative API is on `.grid`.
el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);
Assign before appending where you can. The element builds its grid once, at the end of the task in which it connects, so everything set in that task arrives as one configuration rather than as a series of updates. Setting properties later still works: they go through the live configuration path, but the grid is built empty first and repainted after, which is a visible flash on a large set.
Reading .grid forces the build immediately, so the property is never briefly
null.
Events
el.addEventListener('lattice-cell-changed', (e) => {
console.log(e.detail.key, e.detail.colId, e.detail.value);
});
Every grid event is re-dispatched as a CustomEvent named lattice-
plus the grid name with colons hyphenated, so cell:edit:start becomes
lattice-cell-edit-start. The payload is event.detail. The prefix is
not decoration: the grid emits an event called scroll, and an unprefixed
CustomEvent of that name would be indistinguishable from the platform's own.
Forwarding is a wildcard subscription, so events added to the grid later appear here with no
change to the component.
Light DOM, deliberately. The element renders into itself rather than a
shadow root, because the grid's generated decoration rules are injected into
document.head and the theme stylesheet is a <link> the page
owns: neither crosses a shadow boundary. A shadowed grid would be structurally correct and
completely unstyled, and subtly so: the --lattice-* tokens do inherit
through a shadow root, so the colours would arrive while every pill, bar and heat cell stayed
bare. Light DOM keeps every documented theming route working unchanged.
Events
One bus, forty-odd events. These are the ones most applications actually use; the reference lists them all.
| Event | Carries | Use it for |
|---|---|---|
| ready | {} | First render is done. Fires on a future turn, so you can subscribe on the line after createGrid. |
| cell:changed | { row, key, colId, value, oldValue, undo } | Persisting an edit. undo distinguishes a rollback from a fresh change. |
| selection:changed | { keys } | Enabling a bulk action. |
| range:changed | { ranges } | A status bar showing the sum of what is selected: see selection.summary(). |
| sort:changed / filter:changed | { sort } / { filters } | Reflecting the view in the URL. |
| history:changed | { canUndo, canRedo, undo, redo } | Driving your own undo button. Emitted after the entry is pushed, so the label is right. |
| history:applied | { direction, step } | An action was undone or redone, with which and what. history:changed also fires when a new action is pushed, so it cannot distinguish the two. |
| form:saved | { key, values, changed, unmapped } | Persisting a row edited on a form. unmapped names the fields that are not columns, which the grid reports rather than writes. |
| form:error | { key, error, timedOut } | A row form's load failed or ran out of time. The panel stays open with a retry. |
| view:saved / :removed | { view, views } | Persisting saved views to a server. |
| render:done | { first, last } | Decorating cells from outside. The cell layer rewrites class names on every paint, so anything added before this is erased. |