Export
CSV, JSON, Excel, PDF and clipboard export, data and presentation output, and the optional dependencies.
@workspace/active-grid 11.0.0Status unknowncontent differs from the recorded release 11.0.0baseline ce081dc3
This guide is the package's docs/, included unchanged from
the package as it is in Foundation. Links to the package's internal design
documents keep their text.
What each format guarantees per script, and which route to take when the
grid does not: export-.
Active Grid supports exporting data to CSV, JSON, Excel (.), PDF and
clipboard (TSV). For API surface see
../.
Output classes: data, presentation, clipboard#
Every export surface belongs to exactly one output class (WS-04, PP-013/PP-014). This is the core contract of the export API:
| Surface | Class | Selected how |
|---|---|---|
| CSV / JSON / XLSX | DATA default | options omitted or {mode: 'data'} |
| CSV / JSON / XLSX | PRESENTATION | explicit {mode: 'presentation', locale} |
| PRESENTATION | always — PDF is human output by definition | |
| Clipboard (TSV) | PRESENTATION | always — you copy what you see |
DATA mode (the default)#
DATA export serializes machine/raw semantic values for downstream processing: importing elsewhere, analysis, durable data transfer. Its output is byte-stable under every viewer language — no user or admin presentation setting may ever change it.
Headers and JSON keys are column ids (field keys), not translated header labels.
valueis never consulted. The value comes from the raw cell value (via the column accessor), or from an explicitFormatter meta.callback when the raw value is not the machine value.export Value Numbers stay native numbers (JSON/XLSX) or serialize with
.-decimal and no grouping (CSV). Booleans stay booleans.A declared calendar-day column (
type: 'date'/meta.) exports the machine formsemantic Type: 'date' YYYY-; a declared instant column (MM- DD type: 'date) exports ISO 8601 UTC. An undeclared rawTime' Datefalls back to the same heuristic the chart CSV export uses: UTC-midnight → calendar day, anything else → full ISO instant.A non-empty value that is not readable as its declared semantic fails the export with a RangeError (the JavaScript built-in) instead of exporting something else: an unreadable
datecell (active-i18n D17 amendment) just as an unreadabledatecell. Absence still exports empty.Time A currency column exports its amount as a bare number plus its ISO 4217 identity in a companion column — see below. Never a symbol.
A percent column exports the value in its declared stored unit (
percent): a fraction column exportsSource 0.125, a percentage-points column exports12.5. No silent factor-100 normalization in either direction.An absent value becomes an empty CSV field, JSON
null, or a skipped XLSX cell. Absent is what the cell orchestration uses:null,undefined,''and a non-finite number (Na,N Infinity,-Infinity; TD-28 and active-i18n F3b-9).Strings export as-is — an identifier like
"00123"stays text and keeps its leading zeros.
PRESENTATION mode (explicit)#
Presentation export renders the human-readable form, mirroring the grid's
own cell orchestration: badge labels from filter,
value output for rows that stand for a record (an aggregate uses the
column's declared semantics, DC-1), currency/number/percent formatting through the
same semantic helpers the cells use, translated header labels as
headers/keys. It requires an explicit locale — a locale code or a
presentation-context object (Grid, WS-07), forwarded
untouched to the central formatters — the grid never derives presentation
from anything it resolves itself. Two deliberate differences
from the screen: absent cells export as an empty string (an export file has no
t() for the localized placeholder), and boolean cells export
true/false. Everything else follows the same path the cell takes, which
includes the non-finite numbers: Na and, since active-i18n F3b-9,
±Infinity export as empty, like the placeholder they draw on screen.
import {exportToCSV} from '@workspace/active-grid';
// DATA (default): machine semanticsexportToCSV(rows, grid, 'users.csv');
// PRESENTATION (explicit): what the user seesexportToCSV(rows, grid, 'users.csv', {mode: 'presentation', locale: 'nl'});The built-in context menu exports CSV/XLSX/JSON in DATA mode and PDF in presentation mode.
Clipboard is a presentation copy, not a data contract#
copy (the menu's row copies and Ctrl+C on selected rows) copies what
the screen shows: formatted dates, amounts with their symbol, translated
badge labels. That is deliberate — a copy travels to an email, a chat
message or a spreadsheet the user is looking at, and should match what they
selected visually. It is not the machine-export contract; a consumer
that needs data uses the CSV/XLSX/JSON export in the default DATA mode.
The same holds for the other two keyboard copies: the cell-range copy
(Ctrl+C on a cell selection) and the single-cell copy (Ctrl+C on the
active cell with nothing selected). All three copy routes write the
presentation text through one clipboard TSV boundary: a value a spreadsheet
would run as a formula gets a leading apostrophe, and a value containing a
tab, line break or quote is quoted. A range copy takes the rectangle that
encloses every range, leaves cells outside the ranges empty, leaves out the
grid's own columns and writes no header row; the context menu's "Copy
selection" writes the same block, and "Copy selection with headers" adds one
header row. Every copy, keyboard or menu, reports its outcome once through
meta. / meta..
Currency identity in DATA export#
A currency column (meta.) never exports a symbol.
The amount exports as a bare number and the ISO 4217 identity travels as
data (PP-005/PP-006):
The exporter resolves the identity per row from
meta.(row field) orcurrency Key meta.(fixed contract) — never from a locale.currency Code A companion column
<columnis added right after the amount column, carrying the resolved code per row (Id>_ currency "USD","JPY"). In JSON it is a companion property with the same name.The companion is skipped when the
currencyfield is itself a visible exported column — the identity is then already in the export.Key A cell whose identity cannot be resolved exports an empty companion field: no identity means no identity claim.
Group/footer aggregates carry the companion only when every aggregated row shares one currency (client grouping) or when the column has a fixed
currency(server grouping, where the client cannot prove the shared currency of a server-wide sum). A mixed-currency aggregate exports as a bare number with an empty companion (PP-010).Code
Export Formats#
CSV#
import {exportToCSV} from '@workspace/active-grid';
exportToCSV(rows, grid, 'users.csv'); // DATA modeComma-separated values by default. Every field is wrapped in double quotes — including the header row, so a header containing the separator cannot produce more columns than the data rows carry.
Internal quotes are escaped as
""(RFC 4180). Embedded newlines are kept inside the quoted field.DATA mode: headers are column ids; machine values as described above.
Choosing the separator#
The separator comes from Grid and applies to every
CSV export in that boundary's tree, headers and data rows alike:
<GridDefaults export={{csvSeparator: ';'}}> <App /></GridDefaults>Csv is exactly one of , ; | — a positive list, closed by
the type and re-checked at runtime, since a provider config can come from plain
JavaScript. Anything else is refused (never truncated or trimmed): the export
falls back to a comma and a console. in development says so. Two grids
under two providers export independently.
Quoting does not depend on the separator. Every field is quoted unconditionally, so a value containing the separator, a quote or a newline is safe whichever separator is chosen, and the formula-injection guard below is unaffected.
The clipboard is not covered by this setting. A clipboard copy is TSV and
joins with a tab: that is what Excel expects on paste, while csv is
about a file. The two paths deliberately share no configuration.
Presentation mode: headers are the column
headerlabels; display values.Values that a spreadsheet would execute are neutralized — see Formula injection.
JSON#
import {exportToJSON} from '@workspace/active-grid';
exportToJSON(rows, grid, 'users.json'); // DATA modePretty-printed JSON with 2-space indentation; array of objects, one per row.
DATA mode: keys are column ids; numbers stay numbers, booleans stay booleans,
nullstaysnull; dates are machine strings; currency amounts carry a<id>_companion property.currency Presentation mode: keys are header labels; values are display strings.
Excel (. xlsx)#
import {exportToExcel} from '@workspace/active-grid';
await exportToExcel(rows, grid, 'users.xlsx'); // DATA modeUses the xlsx library (dynamically imported — not in the initial bundle)
and writes typed cells in DATA mode (PP-014: XLSX is more than CSV with
another extension):
| Semantic type | Stored cell | Display format |
|---|---|---|
| text | string cell | — |
| number | numeric cell | — |
| currency amount | numeric cell (+ identity companion) | — |
| percent, fraction | numeric cell with the fraction (0.125) | native % format |
| percent, points | numeric cell with the points (12.5) | none — no % format |
| date (calendar) | typed date cell (serial) | yyyy- |
| dateTime | string cell with ISO 8601 UTC | — |
| boolean | boolean cell | — |
| null/undefined | cell skipped | — |
Two measured decisions worth knowing:
Calendar days cannot shift by timezone. The date serial is computed from the
YYYY-machine form directly (epoch 1899-12-30), never by handing a JSMM- DD Dateto the sheet writer — that path converts through the local timezone and moves a date-only value to the previous day in western zones.Instants stay ISO strings. An Excel date serial carries no timezone, so typing an instant as a date cell would bake the exporting machine's zone into the data. The stable ISO 8601 UTC string is the machine contract (PP-013).
The fraction
%format changes display only. The stored value remains the fraction; Excel's%format shows12,5%for0.125without altering the semantic value. Percentage-points deliberately get no%format — that would show12.5as1250%.
Presentation mode writes display strings as string cells.
The
.extension is added automatically if not provided.xlsx
Sheet direction#
await exportToExcel(rows, grid, 'users.xlsx', {direction: 'rtl'});direction: 'rtl' marks the workbook's first view right to left
(Workbook., in the file right on the sheet view):
the spreadsheet application puts column A on the right. Nothing else changes.
The column order of the data stays logical (column A is still the first
column), the cell values keep their logical Unicode order without direction
controls, and numbers and dates keep their types. ltr, the default, writes
no view. The row menu passes the grid's resolved direction (ADR-054).
No fonts, no styles. The workbook carries data and one view setting. The
writer (SheetJS Community Edition) writes no cell styles, and a workbook's
look belongs to the spreadsheet application, so Grid has
no font field and Grid never reaches it. A branded workbook would
be a styles-capable backend, which is a decision of its own.
PDF#
import {exportToPDF} from '@workspace/active-grid';
await exportToPDF(rows, grid, 'users.pdf', {locale: 'nl'});await exportToPDF(rows, grid, 'users.pdf', { locale: 'nl', fonts, direction: 'rtl',});PDF is human output by definition: it always renders the presentation form
(translated headers, formatted values) and therefore takes a required
locale. direction: 'rtl' puts the first column on the right of the page,
the reading order of an rtl grid; the cell text keeps its logical order.
PDF fonts and refusals#
The PDF backend (jsPDF with jspdf-autotable) draws text without shaping and
without bidi, and encodes UTF-16 code units. What it cannot draw correctly the
grid refuses with a coded Grid; it never emits broken glyphs.
The host owns the fonts. A Grid names families in fallback
order; each face's source (a Grid) is the font's bytes
or a lazy loader that resolves to them, with an optional weight
(1 to 1000, default 400) and style ('normal' or 'italic'). Each family
needs a regular face (weight below 600, style normal). The grid takes no URL,
fetches nothing, keeps no registry and sets no timeout: the loader is the
host's, with its own network rules.
const fonts: GridExportFonts = { families: [ { family: 'Corporate Sans', faces: [ {source: () => loadFont('/fonts/corporate-regular.ttf')}, {source: () => loadFont('/fonts/corporate-bold.ttf'), weight: 700}, ], }, { family: 'Noto Sans CJK', faces: [{source: () => loadFont('/fonts/noto-cjk.ttf')}], }, ],};The same object is an active-charts ChartExportFonts: the two packages own
parallel types with one shape (the grid's lacks embed, because the grid
writes no SVG).
Which set applies, highest first:
the call's
fonts(export);To PDF* the grid's own
export.(row menu);fonts Grid'Defaults export.(row menu);fonts none.
'inherit' on the call or on the grid means "no set from here down". A PDF
cannot inherit the page's web fonts (the browser never hands them over as
TrueType), so it then draws in Helvetica. A standalone call reads no
Grid; the row menu reads the set at the click.
What each cell draws in. Every text unit (a header cell, a data cell, a group label, a group-footer cell) is checked before the document is built:
The guard. Text a backend without shaping cannot draw is refused, whatever the fonts: Arabic, Hebrew, Syriac, Thaana, N'Ko, Mongolian, Tibetan, Thai, Lao, Khmer, Myanmar and the Indic/Brahmic scripts; a combining mark in the drawn text; ZWJ, ZWNJ and variation selectors; emoji; any code point beyond the Basic Multilingual Plane.
'EXPORT_, withTEXT_ UNSUPPORTED' reasonand, for a script,script.Without a set, the text must be WinAnsi (Latin-1 plus the Windows-1252 additions such as
€), drawn in Helvetica. Anything else is'EXPORT_.FONT_ REQUIRED' With a set, the unit is drawn in the first family whose face covers every code point of it, read from jsPDF's own parse of the font. A unit no family covers is
'EXPORT_(with theGLYPH_ COVERAGE' familiestried). One cell is never split across families: a cell mixing scripts needs one family that covers all of it.
The header row and the group rows are bold: they ask for the family's bold
face. A missing face falls back deterministically (exact, then the same style
in the other weight, then the same weight in the other style, then regular)
and is never synthesised. Only TrueType faces embed
('EXPORT_ names what other bytes are). Faces are
loaded before the document is built; concurrent exports share one load of a
face object, and a failed load is retried by the next export.
The same pipeline serves export, export and
export; the server-grouped route loads its backend and
fonts before it asks the server for a single group.
Shaped, selectable PDF for complex scripts is not a package promise: it is a host route (for example browser print). CSV, JSON and XLSX carry every script losslessly.
Export errors#
Every refusal and every missing backend is a Grid: code is
stable, the message is prose. Its metadata never holds exported text, because
a cell can hold personal data and hosts log these errors: at most one code
point, a script name, the host's family name, what a font's bytes are, the
module that is missing, the families tried. A low-level failure (a host
loader's rejection, jsPDF's parser) is the cause.
code | When | Metadata |
|---|---|---|
'EXPORT_ | xlsx, jspdf or jspdf- could not be loaded | format, missing, cause |
'EXPORT_ | PDF text outside WinAnsi and no set | format, code |
'EXPORT_ | a face is not TrueType | format, family, detected |
'EXPORT_ | the set has no family, or a family has no regular face | format, family |
'EXPORT_ | empty or non-byte source, unreadable bytes, bad weight or style | format, family, cause |
'EXPORT_ | a host loader rejected | format, family, cause |
'EXPORT_ | the PDF guard refused a text | format, reason, script, code, normalizable |
'EXPORT_ | no family covers a whole cell | format, code, families |
The codes are active-charts' ChartExportError codes where the meaning is the
same. A refusal produces no file: a programmatic call rejects, and a row-menu
export reports it to on.
Optional dependencies#
xlsx, jspdf and jspdf- are optional dependencies of the
grid. Rendering and importing the package never load them; the XLSX and PDF
exporters load them on first use. An installation without one still renders
every grid, and the export that needs the missing module fails with
'EXPORT_ naming it. There is no fallback. A host that
offers no XLSX or PDF export leaves them out and removes the format from
export.. A bundler still resolves the dynamic import at build time,
and Vite/Rollup refuses the build when the module is absent (measured in the
portable-runtime P9 clean-room consumer). A host that leaves them out declares
them external (build.); the browser then fails the
import at export time and the grid reports 'EXPORT_.
Clipboard Copy#
import {copyToClipboard} from '@workspace/active-grid';
const success = await copyToClipboard(rows, grid, { locale: 'nl', includeHeaders: false,});Tab-separated values (TSV format) in the presentation form — see the output-class contract above.
Pasteable into Excel, Google Sheets, or any spreadsheet.
include(default:Headers false) adds a header row with the visible header labels.Returns
trueon success,falseon failure.Uses the
navigator.API.clipboard. write Text Minimal quoting. Only a value containing a tab, a newline or a
"is wrapped in quotes; plain values stay plain, so pasting into a text editor gives clean output. Without this, a single cell containing a tab shifted every following column of that row. The quoting is lossless: a quoted tab or newline arrives inside its own cell.Values that a spreadsheet would execute are neutralized — see Formula injection.
Formula injection#
A spreadsheet executes a cell whose text starts with certain characters. A CSV or clipboard export therefore neutralizes any value that a spreadsheet would read as something other than the literal text it contains, by prefixing a single apostrophe. This is always on and has no opt-out.
It applies to CSV files and clipboard copy, and covers data cells, column headers, group labels and group-footer aggregates alike — the boundary is defined by where the output goes, not by where the value came from.
=1+1 -> '=1+1=cmd|' /C calc'!A0 -> '=cmd|' /C calc'!A0-Notitie over de klant -> '-Notitie over de klant (would render #NAME?)+Notitie over de klant -> '+Notitie over de klant-1+1 -> '-1+1 (would evaluate to 0)@SUM(1,1) -> '@SUM(1,1)The apostrophe stays visible in the cell. That is deliberate: it reads as a marker, where a leading space would read as a defect.
What it deliberately leaves alone#
Your amounts are not affected. The rule tests behaviour, not a list of
characters, so machine numbers (-1234.5) and the output of format
and the percent formatters pass through untouched in every supported
locale — a negative amount is read by the spreadsheet as a negative number,
not as a formula:
-1234.5 (machine form) unchanged-1.234,5 (nl-NL, de-DE) unchanged-1,234.5 (en-US) unchanged-1 234,5 (fr-FR) unchanged-50% / -50 % unchanged-42 unchangedFree text that merely starts with a sign is also left alone when it cannot form
an expression — -50% korting afgesproken, -2 (zie bijlage), and a lone -
used as a "no value" placeholder all export unchanged.
A fixed list of dangerous prefixes was rejected precisely because it would have prefixed every negative amount in the grid.
Not applied to XLSX, JSON or PDF#
XLSX needs no neutralization: the writer emits typed cells, so a string value
starting with = is stored as text and is never evaluated. JSON and PDF have
their own correctness rules, where an injected apostrophe would be wrong.
Known limits#
Measured on Excel (Office 16, nl-NL) for the CSV and paste routes. LibreOffice Calc and Google Sheets were not measured.
@does not evaluate on Office 16 and is neutralized as hardening for engines that may.A leading
+on a value that reads as a number is stripped by Excel itself on paste (+1234becomes1234). That is number parsing, not formula execution, so the sanitizer does not touch it. Relevant only if a column ever carries a value with a meaningful plus, such as a phone country code.
What Gets Exported#
Included#
All visible columns (in their current order), plus the currency companion columns in DATA mode.
The rows of the current grid page (filtered, sorted, within
page) -- see Export scope. A programmatic caller passes whatever rows it wants.Size DATA mode: raw semantic values. Presentation mode: display values.
Excluded#
System columns:
selection,expander,actions— automatically excluded.Hidden columns: only visible columns are exported.
Value formatters in DATA mode: the machine value is exported, never the formatted display string. Use
meta.when the raw value is not the machine value.export Value
Export precedence (DATA mode)#
For each cell, the exporter resolves the machine value in this order:
meta.— explicit machine value, when provided;export Value(value, row) the raw cell value via the column accessor;
serialization per declared
meta.(currency/number/percent/date/dateTime);semantic Type serialization by runtime type (number, boolean,
Dateheuristic, text).
Rendered React content and value output never participate.
Export scope (context menu)#
The row menu exports what the reader has in front of them, and nothing else. There are two scopes, and both mean the same thing in every mode:
| Option | Rows exported |
|---|---|
| Export selection (N) | Only the rows that are actually selected locally |
| Export view (N) | The rows of the CURRENT paginated grid page |
| Export loaded rows (N) | Infinite scroll only: the blocks loaded so far |
Export view is the current page: after filtering, after sorting, within the
current page and page, in the order the screen shows. page
therefore bounds the export -- page size 10 exports at most 10 rows, and the
last page exports only what actually remains. The meaning is identical in
mode="client" and mode="server", because both read the same paginated row
model (grid.). A grid whose pagination control is hidden
(pagination={{enabled: false}}) still renders one page-sized slice, and the
export follows that slice.
The count next to the label and the number of data rows in the file are the same array. The menu resolves its label and its rows in one step and then counts the very array it writes, so the two cannot drift apart.
Infinite scroll has no page index, so it carries its own label over the rows already fetched. It never fetches missing blocks to satisfy an export.
Outcome: completion and failure#
A menu export is awaited by nobody, so its outcome reaches the host through
two callbacks on the export prop (ADR-046):
<ActiveGrid // ... export={{ formats: ['xlsx', 'csv'], onExportComplete: (format, scope) => notifyDone(format, scope), onExportError: (format, error, scope) => notifyFailed(format, error), }}/>| Callback | When |
|---|---|
on | the file was handed to the browser |
on | no file was produced; error is the cause |
Never both, never the wrong one. scope is a Grid: 'page',
'loaded' or 'selection', the row set the file holds. Without
on a failure goes to the developer log; it is never swallowed.
The names and first arguments are those of the chart's on /
on, and the two packages use the same words for data export,
presentation export, completion, failure and scope (ADR-046 § 6).
A refusal of the grid itself (a PDF text it cannot draw, a font set it cannot
use, a missing optional backend) arrives in on as a
Grid with its code (Export errors).
A programmatic call needs no callback: a synchronous function (export,
export) throws, an asynchronous one rejects, and a return means the
file was handed to the browser.
No cancellation. No export path is cancellable; the internal
Abort of the *Server functions is never aborted.
Cancellation belongs to a full-dataset export, which is a separate product
feature (below).
There is no generic "Export all"#
It was removed on 2026-08-31, not renamed. Two measured reasons:
Export viewused to readget. In client mode that is every filtered row across ALL pages --Filtered Row Model() Export view (152)beside a paginator reading1 - 10 of 152-- and in server mode it is only the current page, becausemanualfalls that model back onto the page data. One label, two meanings.Filtering Export allpromised "all rows, ignoring filters" and, measured on a server-mode grid of fifteen users with a filter active, produced a file with the seven FILTERED rows while the label readExport all (7).
A full-dataset export is a separate product feature: it needs its own permission, accountable-read evidence, volume ceiling, progress, cancellation and error handling. None of that belongs behind a row menu, and the grid does not own the rows it would have to promise.
Grid Export Row Scope#
type GridExportRowScope = 'page' | 'loaded';Which row set the built-in row menu can honestly offer. page is the current
paginated page and means the same in client and server mode; loaded belongs
to infinite scroll, where no page exists and the model carries every block
fetched so far. The grid derives it from pagination. and declares it on
meta.; a consumer never sets it.
There is deliberately no value for "the whole dataset": the grid does not own those rows, and a label promising them would be untrue.
Programmatic Export#
The export functions accept any array of Row<TData> objects and a
Grid<TData> instance:
import {exportToCSV, exportToExcel} from '@workspace/active-grid';
// Export all filtered rows (DATA mode)const filteredRows = grid.getFilteredRowModel().rows;exportToCSV(filteredRows, grid, 'filtered-data.csv');
// Export selected rows as the user sees themconst selectedRows = grid.getSelectedRowModel().rows;await exportToExcel(selectedRows, grid, 'selected-data.xlsx', { mode: 'presentation', locale: 'nl',});File Download Mechanism#
All export functions create a Blob, generate a temporary object URL, and
trigger a download via a dynamically created <a> element:
const blob = new Blob([content], {type: mimeType});const url = URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;link.click();URL.revokeObjectURL(url);This works in all modern browsers without requiring a server endpoint.
Grouped Exports#
For grouped data (client-side or server-side), the package exports parallel functions that preserve group structure in the output:
import { exportToCSVGrouped, exportToJSONGrouped, exportToExcelGrouped, exportToCSVServerGrouped, exportToJSONServerGrouped, exportToExcelServerGrouped, type ServerGroupedExportConfig,} from '@workspace/active-grid';These are programmatic only: the row context menu does not use them. A grouped export writes the whole group tree, which is not one page, so wiring it into the menu would put the label, the count and the file back out of step. The menu always exports the current page's data rows; group header rows are not data and are excluded from the count and from the file.
*Groupedvariants apply tomode="client"grids with client-side grouping.*Servervariants take aGrouped Serverparameter for server-side grouped pagination, which the caller assembles itself.Grouped Export Config Group footers export raw aggregate values in DATA mode; currency aggregates follow the shared-currency rule above. Presentation mode formats aggregates like the on-screen group footer.
Indentation of nested groups is presentation: DATA mode emits unindented cells (an indented number is no longer a number) and conveys the structure through the group-header and group-footer rows; the grouped JSON is hierarchical in both modes.
Standalone boundary#
The exporter reasons exclusively from generic column semantics
(semantic, currency/currency, percent,
meta.) and from the locale the caller supplies for presentation
output. active-grid never resolves tenant, administration or application
settings itself — any application-specific presentation context enters
through the grid's existing configuration surface (column, locale,
explicit export options).