api reference
Export and saved views: JavaScript Data Grid API
grid.export to Excel and CSV, grid.import from a CSV or a pasted block, grid.state, grid.views, pagination and scroll.
API reference › Export and saved views
grid.export
| Method | Returns | Description |
|---|---|---|
| csv(opts) | string | Blob | Fields sanitised against formula injection. |
| excel(opts) | Promise<Blob> | Real .xlsx, written without a ZIP dependency. Large exports stream. |
| clipboard(opts) | Promise | TSV, with the grid's own paste parser as its counterpart. |
| print(opts) | void | Switches virtualisation and pinning off for the printed document. |
| rows(mode) | Row[] | 'all', 'visible' or 'selected'. |
Excel value conversion. A data type may declare toExcelValue and excelKind, because a spreadsheet's number formats are not free-form. A time of day is written as a fraction of a day under hh:mm:ss; a duration as days under [h]:mm:ss, where the brackets are what stop Excel wrapping at 24 hours. Both stay numeric, so they still sort and subtract in the sheet.
IP columns are stored as packed integers so they sort as addresses, and declare excelKind: 'string' so the dotted form is what reaches the file. Radix columns export decimal: OOXML cannot express base 16, and staying numeric was judged worth more than display fidelity.
grid.import
The mirror of grid.export: rows coming in from delimited text - a CSV or TSV file, or the tab-separated block a spreadsheet puts on the clipboard. The pipeline is parse, infer each column's type, map the columns onto the grid's own fields, then preview and confirm. The API is always present; set config.import to add the DOM affordances (a "Import rows from CSV…" cell-menu item, a file drop target, and paste).
| Method | Returns | Description |
|---|---|---|
| preview(text, opts?) | ImportPreview | Parse, infer and map without changing the grid - the columns, the coerced records, a sample and any warnings a confirm dialog needs. |
| csv(text, opts?) | object[] | Parse delimited text into coerced record objects, the inverse of export.csv. |
| apply(input, opts?) | ChangeResult | null | Add (mode: 'append', the default) or replace (mode: 'replace') the grid's rows from text, a preview or a record array. A client-side operation, so it applies to a memory-source grid; on any other source it declines rather than show rows that cannot persist. |
Excel. .xlsx is read too, through the platform's own inflate (DecompressionStream) and a tag scanner in the core - the same pipeline as CSV, only async. A multi-sheet workbook reads its first visible worksheet by default; pass a sheet name or zero-based index to read another, and list the workbook's sheets (hidden ones flagged) with xlsxSheets.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'id' }, { field: 'age', type: 'integer' }],
rows: [{ id: 'seed', age: 1 }],
rowKey: 'id',
// Opt in to the DOM affordances; the grid.import API is present either way.
import: true,
});
// Preview changes nothing - it is what a confirm dialog shows.
const preview = grid.import.preview('id,age\nx,20\ny,30');
// Confirming appends the previewed rows; `age` arrives coerced to a number.
grid.import.apply(preview);
return `${preview.rowCount} previewed, ${grid.rows.count()} rows`; // 2 previewed, 3 rows
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
columns: [{ field: 'id' }, { field: 'age', type: 'integer' }],
rows: [{ id: 'x', age: 20 }, { id: 'y', age: 30 }],
rowKey: 'id',
});
// Write a two-sheet workbook so there are real .xlsx bytes to read back.
const out = await grid.export.excel({
sheets: [{ name: 'Summary' }, { name: 'Data' }],
});
const bytes = new Uint8Array(await out.arrayBuffer());
// List the workbook's sheets, in order, hidden sheets flagged.
const sheets = await grid.import.xlsxSheets(bytes);
// Read one of them by name - a zero-based index ({ sheet: 1 }) works too.
const preview = await grid.import.previewXlsx(bytes, { sheet: 'Data' });
return `${sheets.map((s) => s.name).join(', ')} / ${preview.rowCount} rows`;
grid.state
| Method | Returns | Description |
|---|---|---|
| get(opts?) | GridState | Versioned and serialisable: columns, columnOrder, filters, quick, sort, group, pivot, expanded, selection, pagination. scroll is omitted unless called with { scroll: true } - it is transient UI state, not view state, so a saved view does not reopen the grid scrolled to wherever it was when captured. |
| apply(state, opts?) | object | Restore a view. Returns a report of anything it could not apply (a column that no longer exists, for instance) rather than failing silently. A scroll key in state is restored only when opts.scroll is also true; otherwise it is left alone even when present. |
| baseline() | GridState | null | The state the grid started in, captured once after config.state and any default view, so the baseline is the grid you shipped, not the one before your own configuration ran. |
| reset() | object | null | Put the grid back to that baseline, as one undo entry. Clears anything the baseline does not mention, including the quick filter. |
| modified() | boolean | Whether anything has changed since construction. Lets a "restore" control disable itself rather than offering an action that would do nothing. |
Scroll is opt-in, on both sides. It is transient UI state, not view state, so a saved view, the baseline and serialiseState's diff never carry it: get() omits it unless called with { scroll: true }, and apply() restores a scroll key only when it is present and apply is also called with { scroll: true } - so an old persisted state that still has one does not reopen the grid scrolled away from the top.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ id: 'n', field: 'n' }],
rows: [{ id: '1', n: 'a' }],
});
const hasScrollByDefault = 'scroll' in grid.state.get();
const hasScrollOptedIn = 'scroll' in grid.state.get({ scroll: true });
const ignored = grid.state.apply({ version: 2, scroll: { top: 220, left: 0 } });
const scrollIgnored = !ignored.applied.includes('scroll');
const applied = grid.state.apply({ version: 2, scroll: { top: 220, left: 0 } }, { scroll: true });
const scrollApplied = applied.applied.includes('scroll');
const result = [hasScrollByDefault, hasScrollOptedIn, scrollIgnored, scrollApplied].join(',');
grid.destroy();
return result;
grid.views
Named states the user can return to. Views supplied in config.views.saved are defined views: listed apart in the picker, and neither renamable nor deletable, refused by the model as well as hidden in the interface. Views the user saves are their own and carry rename, share, default and delete.
createGrid(el, {
views: {
saved: [{ id: 'escalations', name: 'Escalations', description: 'Worst SLA first',
state: { filters: { col: 'statusId', op: 'eq', value: 4 },
sort: [{ col: 'utilisation', dir: 'desc' }] } }],
allowSave: true, // false removes the save form entirely
local: true, // saved views live in this browser's localStorage, no backend
// storage: { read, write }, // or bring your own backend; see the note below
},
});
| Method | Returns | Description |
|---|---|---|
| list() / get(id) | object[] / object | |
| save(name, opts?) | object | Captures the current state. opts: id, description, shared, isDefault. |
| apply(id) | object | null | One undo entry. Resets to the baseline first, so a view is a destination rather than a patch, the same view gives the same grid whatever was applied before it. |
| rename(id, name) | object | null | Null for a defined view. |
| remove(id) | boolean | False for a defined view. |
| setDefault(id) | object | null | null clears it. A default view is applied on load, without recording an undo entry. |
| export(id) / import(json) | object | A JSON payload. What "sharing" means is yours to decide. |
| diff(id) | object | null | What applying a view would change. |
| reload() | void | Re-read from storage, discarding what is in memory. |
| activeId | string | null |
The grid makes no network calls. storage.write is a synchronous mirror. To persist to a server, listen for view:saved, view:renamed, view:removed and view:default: each carries the one view that moved, so you can send a single record rather than diffing two lists. Because the grid does not track whether your write landed, a failed request leaves the view visible locally: catch it and call views.reload().
views.local is the no-backend option: true stores views under a default localStorage key, shared by every grid on the origin; { key: '…' } picks a key of your own, for more than one grid whose views should stay apart. Given alongside an explicit storage, storage wins and local is ignored, with a console warning, the two are never merged. Built on createLocalViewStorage, exported for direct use (a custom key, or a different Storage-shaped backing such as sessionStorage) without the local shorthand.
grid.pagination
A window over the rows the query already produced, not another query. A page change re-slices; it does not re-filter, re-sort or re-group, so paging a million rows costs nothing beyond the repaint.
| Method | Returns | Description |
|---|---|---|
| get() | { page, pageSize, total, pageCount, counting } | total is the filtered row count, so it moves when a filter does. On a paged or remote source it is null until the server's total arrives, with counting true while the first answer is still on its way. |
| set({ page?, pageSize? }) | void | Move, resize, or both. pageSize: 0 turns paging off and shows everything. Emits page:changed once the rows have moved. |
Paging over a paged or remote source
A paged or remote source holds only the rows it has fetched, so there is
nothing to slice. Instead, page p is the row window
[p × pageSize, (p + 1) × pageSize) of the server's answer to the current
query, and moving to a page fetches the blocks that window needs - one request when the page
size equals the source's own pageSize. The viewport scrolls within the page, rows still
in flight draw as loading rows, and rows.get(0) is the first row of the page, exactly as
on a memory grid. rows.count() is the page on screen; rows.matchCount(),
rows.totalCount() and pagination.get().total are the whole result.
- Configure both.
pagination: { enabled: true, pageSize: 250 }turns the pager on;source: { mode: 'paged', pageSize: 250, fetch }sets the block the server is asked for. Matching them makes every page one request. A headless grid honours the configured page size too, with no pager to apply it. - A new query starts at page 0. A sort, a filter or a quick filter changes the list the
server answers, so the page goes back to the first one and
page:changedsays so. - No total yet is not the end. While
totalisnull, the pager keeps "next" enabled andpageCountcounts the pages known so far plus the next one. - A source with no page model refuses by name. A
streamor custom source that cannot window warns once (pagination:source-unsupported), shows no pager and keeps scrolling.
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const asked = [];
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'id' }, { field: 'name' }],
pagination: { enabled: true, pageSize: 250 },
source: {
mode: 'paged', pageSize: 250,
fetch: async ({ range }) => {
asked.push(`${range.start}-${range.end}`);
const rows = [];
for (let i = range.start; i < Math.min(range.end, 1000); i++) rows.push({ id: i, name: `row ${i}` });
return { rows, total: 1000 };
},
},
});
const heard = [];
grid.on('page:changed', (e) => heard.push(e.page + 1));
const landed = new Promise((resolve) => grid.once('rows:changed', resolve));
grid.pagination.set({ page: 2 }); // the third page: rows 500 to 749
await landed;
const { page, pageCount, total } = grid.pagination.get();
const first = grid.rows.get(0).data.id;
grid.destroy();
return `page ${page + 1} of ${pageCount}, rows from ${first}, ${total} total, fetched ${asked.join(' ')}, page:changed to ${heard}`;
grid.scroll
| Method | Returns | Description |
|---|---|---|
| position() | { top, left } | |
| toRow(row, align?) | void | align: 'start', 'centre', 'end'. |
| toColumn(id) | void | |
| to(at) | void | Scroll to { top, left }. left is the logical offset: zero at the content's start whichever way the grid reads. |
| toCell(row, colId, align?) | void | Scroll a cell into view, both axes in one call. row is a row key or a display index. |