XLSX Export
A real workbook, written without a dependency: several sheets, styles, Excel's own conditional formatting rules, formula cells, and 200k rows for about 26 MB of heap because it writes in chunks.
@sv5ui/datagrid/xlsx, so a grid that never exports a workbook never pays for the code that writes one. Half of
them do not take a grid at all. Exporting a Grid
The visible columns, in their current order, filtered and sorted the way the reader left them. Sort or filter the grid below, then build the workbook and watch the byte count move.
A grid showing a pivot exports the crossing rather than the rows it folded away: the row fields, the generated columns, and the gathered rows as they stand, a total row included. A nested tier the reader has left shut is not written, since it is not on the grid either.
import {
conditionalFormatting, createDataGrid, filtering, selection, sorting
} from '@sv5ui/datagrid';
import { buildGridXlsx, downloadGridXlsx } from '@sv5ui/datagrid/xlsx';
const grid = createDataGrid<Line>({
columns,
data: lines,
getRowId: (line) => String(line.id),
features: [
sorting(),
filtering(),
selection(),
conditionalFormatting({ rules: [{ kind: 'colorScale', column: 'price' }] })
]
});
// The visible columns, in their current order, filtered and sorted the way
// the reader left them.
await downloadGridXlsx(grid, { filename: 'orders.xlsx', sheetName: 'Orders' });
buildGridXlsx(grid, { sheetName: 'Orders', conditionalFormatting: true });
// Uint8Array, 6016 bytes over these six rows
// Narrow it:
await downloadGridXlsx(grid, {
columnIds: ['sku', 'quantity', 'total'], // and in this order
selectedOnly: true, // only the ticked rows
freezeHeader: true, // the header stays put on scroll
autoFilter: true // Excel's own filter dropdowns
});
// Or take the bytes and do something else with them:
const bytes = buildGridXlsx(grid); // Uint8Array
await fetch('/api/reports', { method: 'POST', body: bytes });Types Carry Across
A column's type goes into the workbook, so a spreadsheet sorts and filters it as what it is rather than as text. That is the difference between an export you can pivot and one you cannot: the date column above lands as a real date serial, not as the string the cell drew.
const columns: ColumnDef<Line>[] = [
{ id: 'sku', header: 'SKU' },
{ id: 'units', header: 'Units', type: 'number' },
{ id: 'price', header: 'Unit price', type: 'currency' },
{ id: 'share', header: 'Share', type: 'percent' },
{ id: 'orderedOn', header: 'Ordered on', type: 'date' }
];
buildGridXlsx(grid, {
value: (row, column) =>
column.id === 'total' ? row.units * row.price : undefined
});
// A date lands as a real serial rather than the string the cell drew, which
// is the difference between an export you can pivot and one you cannot.Styles
The grid applies a default from each column's type, and you can override at three levels: the header, a whole column, or a single cell with its value in hand.
BUILT_IN_STYLES
Read live from the export at runtime, so this table cannot drift from the package.
| Style | Number format |
|---|---|
header | bold |
date | yyyy\-mm\-dd |
datetime | yyyy\-mm\-dd\ hh:mm |
currency | #,##0.00 |
percent | 0.00% |
integer | #,##0 |
import { BUILT_IN_STYLES } from '@sv5ui/datagrid/xlsx';
// The defaults, which the grid applies from a column's type:
// header bold
// date yyyy-mm-dd
// datetime yyyy-mm-dd hh:mm
// currency #,##0.00
// percent 0.00%
// integer #,##0
buildGridXlsx(grid, {
headerStyle: { font: { bold: true, color: '#FFFFFF' }, fill: '#1F2937' },
columnStyle: (column) =>
column.id === 'total' ? { font: { bold: true } } : undefined,
// Per cell, so the style can read the value.
cellStyle: (value, row, column) =>
column.id === 'delta' && Number(value) < 0
? { font: { color: '#B91C1C' } }
: undefined
});
// An XlsxStyle is font, fill, border, alignment and numberFormat. Colours are
// any CSS hex; they are converted to Excel's ARGB on the way in.Conditional Formatting, as Excel's Own
Not as flattened colours. Open the file and the rules are still rules: they recalculate when a value changes. Excel's palette is not your theme's, so the colours are named separately.
DEFAULT_FORMAT_COLORS
Also read live. Pass an XlsxFormatColors of your own to change any of them.
| Key | Default |
|---|---|
scaleFrom | #FFFFFF |
scaleTo | #3B82F6 |
bar | #638EC6 |
highlightFill | #FFEB9C |
highlightText | #9C6500 |
// The rule has to be registered on the grid for there to be anything to
// carry across:
conditionalFormatting({ rules: [{ kind: 'colorScale', column: 'price' }] });
buildGridXlsx(grid, { conditionalFormatting: true });
// It exports as Excel's own rule, not as flattened colours: open the file
// and it still recalculates when a value changes.
// Excel's palette is not your theme's, so the colours are named separately:
import { DEFAULT_FORMAT_COLORS } from '@sv5ui/datagrid/xlsx';
buildGridXlsx(grid, {
conditionalFormatting: {
scaleFrom: '#FFFFFF',
scaleTo: '#3B82F6',
bar: '#638EC6',
highlightFill: '#FFEB9C',
highlightText: '#9C6500'
}
});
// A rule the grid computes from whole-column statistics travels as the
// equivalent Excel rule, so the workbook recomputes it over the rows it holds
// rather than freezing what the browser worked out.Large Exports
The sheet is written in chunks rather than assembled as one string, which is what keeps a large export inside the heap. Past a few thousand rows use the async builder: it yields between batches, so the tab keeps painting and a progress bar can move.
It also writes a much smaller file. The synchronous builder stores its entries; the async one deflates them, which is only worth doing off the main thread. Both are valid workbooks, so this is a reason to prefer the async builder even when blocking would not have mattered.
The button below builds the same 500 rows both ways and reports what each one weighed, so the comparison is measured in your browser rather than quoted from mine.
const bytes = await buildGridXlsxAsync(grid, {
batchSize: 50, // the demo above, so onProgress reports ten times
onProgress: (loaded, total) => {
done = loaded;
expected = total;
}
});
// Same 500 rows, both builders. The demo above measures this in your own
// browser rather than trusting the numbers written here:
buildGridXlsx(grid).length; // 111_360, stored
(await buildGridXlsxAsync(grid)).length; // 16_331, deflated - about a seventh
await downloadGridXlsx(grid, { loadedOnly: true });
// The async builder yields between batches and deflates its entries, which
// is only worth doing off the main thread.Exporting What the Server Has
A grid in server mode holds a hundred rows of five thousand, so "export the grid" is two different requests. The builders will not guess which one you meant: the synchronous one refuses outright, and the message names both ways forward.
loadedOnly: true writes what is on screen and says so. The async builder pages the data source itself, in batches,
and reports as it goes: watch the request log fill while it walks the five thousand.
No data
The last few requests
- Nothing yet. Export something.
import { buildGridXlsx, buildGridXlsxAsync, downloadGridXlsx } from '@sv5ui/datagrid/xlsx';
// The grid holds a hundred of five thousand rows, so "export the grid" is
// two different requests and the builders do not guess which one you meant.
// Refuses. Nothing in memory is the whole set, and writing a hundred rows
// into a file called everything.xlsx is the wrong kind of success.
buildGridXlsx(grid);
// Error: xlsx: this grid uses rowModel: "server", so only the rows currently
// held in memory are available synchronously. Use await downloadGridXlsx(grid),
// which pages the data source for the whole set, or pass { loadedOnly: true }
// to export just what is on screen.
// What is on screen, said out loud.
buildGridXlsx(grid, { loadedOnly: true });
// Or the whole set: this pages the data source itself, in batches, and
// reports as it goes.
await buildGridXlsxAsync(grid, {
batchSize: 1000,
onProgress: (loaded, total) => (progress = `${loaded} of ${total}`)
});
await downloadGridXlsx(grid, { fileName: 'people.xlsx', batchSize: 1000 });
// A selection is exempt too: selectedOnly means the rows you picked, which
// are by definition ones you have.A Workbook With No Grid
createWorkbook takes columns and rows, and an array of them makes several sheets. Nothing about it knows what
a grid is.
application/vnd.openxmlformats-officedocument.spreadsheetml.sheetimport { createWorkbook, XLSX_MIME } from '@sv5ui/datagrid/xlsx';
// No grid involved. This is a workbook writer that a grid happens to use.
const bytes = createWorkbook({
sheetName: 'Q1',
columns: [{ header: 'SKU', width: 14 }, { header: 'Units' }, { header: 'Revenue' }],
rows: [
['DG-100', 3, 121.5],
['DG-210', 7, 539]
],
freezeHeader: true,
autoFilter: true
});
// Several sheets: pass an array.
createWorkbook([
{ sheetName: 'Q1', columns, rows: q1 },
{ sheetName: 'Q2', columns, rows: q2 }
]);
// A sheet name Excel will not take is sanitized rather than rejected: the
// characters / \\ : * ? [ ] each become a space, and the name is clipped to
// Excel's 31.
new Blob([bytes], { type: XLSX_MIME });Cells, Columns and Dates
The small helpers, for building a sheet by hand. Every value below is computed on this page rather than written into it.
columnLetter(0) / columnLetter(26) | A / AA |
cellRef(0, 0) / cellRef(9, 2) | A1 / C10 |
toSerialDate(1900-01-01) | 2 |
toSerialDate(2026-01-01) | 46023 |
toSerialDate(2026-01-01 12:00) | 46023.5 |
import { cellRef, columnLetter, toSerialDate } from '@sv5ui/datagrid/xlsx';
columnLetter(0); // 'A'
columnLetter(26); // 'AA'
cellRef(0, 0); // 'A1' row is zero-based, the reference is not
cellRef(9, 2); // 'C10'
// Excel counts days from 1899-12-30, and the time of day rides along as the
// fraction. Read from local parts, like every other date in the grid.
toSerialDate(new Date(1900, 0, 1)); // 2
toSerialDate(new Date(2026, 0, 1)); // 46023
toSerialDate(new Date(2026, 0, 1, 12)); // 46023.5
// A formula cell, rather than a value:
const rows = [['DG-100', 3, 40.5, { formula: 'B2*C2' }]];.5. Like
every other date in the grid it is read from local parts, never through toISOString, which names the previous day west of Greenwich. ExportXlsxOptions
Everything buildGridXlsx, buildGridXlsxAsync and downloadGridXlsx accept.
| Option | What it does |
|---|---|
filename / sheetName | The download's name, and the sheet's. A name Excel will not take is sanitized rather than rejected |
columnIds | Which columns, and in what order. Defaults to the visible ones as they stand |
rows / selectedOnly | Specific nodes, or only the ticked rows |
freezeHeader / autoFilter | Freeze the header row, and give Excel its own filter dropdowns |
value | What a cell contributes. Return undefined to leave it to the grid |
headerStyle / columnStyle / cellStyle | An XlsxStyle for the header, per column, or per cell with the value in hand |
conditionalFormatting | true to carry the rules across, or an XlsxFormatColors to pick Excel's colours |
loadedOnly | Under a server row model, write the page the grid holds rather than fetching the rest |
onProgress / batchSize | For the async builder, so a large export can show its progress |