# Export Plugin

> Export grid data to CSV or other formats.

The Export plugin lets users download grid data as CSV, JSON, or other formats with a single click or API call. Great for reporting, data backup, or letting users work with data in Excel.

## Installation

```ts
import '@toolbox-web/grid/features/export';
```

## Basic Usage

Enable the feature and call `exportCsv()` or `exportJson()` when you're ready to download. You can also combine it with the selection feature to export only selected rows.

#### TypeScript

```ts
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
    { field: 'department', header: 'Department' }
  ],
  features: {
    export: {
      fileName: 'employees',
      includeHeaders: true,
    },
  },
};

// Trigger export via button click
document.getElementById('export-btn').addEventListener('click', () => {
  const plugin = grid.getPluginByName('export');
  plugin.exportCsv();
});
```

#### React

```tsx
import '@toolbox-web/grid-react/features/export';
import { DataGrid, useGrid } from '@toolbox-web/grid-react';

function EmployeeGrid({ data }) {
  const { ref, element } = useGrid();

  const handleExport = () => {
    element?.getPluginByName('export')?.exportCsv();
  };

  return (
    <>
      <button onClick={handleExport}>Export CSV</button>
      <DataGrid
        ref={ref}
        rows={data}
        columns={[
          { field: 'name', header: 'Name' },
          { field: 'email', header: 'Email' },
          { field: 'department', header: 'Department' },
        ]}
        export={{ fileName: 'employees', includeHeaders: true }}
        style={{ height: '400px' }}
      />
    </>
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/export';
import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';

const data = [
  { name: 'Alice', email: 'alice@example.com', department: 'Engineering' },
  { name: 'Bob', email: 'bob@example.com', department: 'Marketing' },
];

const { gridElement } = useGrid();

const handleExport = () => {
  gridElement.value?.getPluginByName('export')?.exportCsv();
};
</script>

<template>
  <div>
    <button @click="handleExport">Export CSV</button>
    <TbwGrid :rows="data" :export="{ fileName: 'employees', includeHeaders: true }">
      <TbwGridColumn field="name" header="Name" />
      <TbwGridColumn field="email" header="Email" />
      <TbwGridColumn field="department" header="Department" />
    </TbwGrid>
  </div>
</template>
```

#### Angular

```typescript
// Feature import - enables the [exportFeature] input
import { GridExportDirective } from '@toolbox-web/grid-angular/features/export';
import { Component } from '@angular/core';
import { Grid, injectGrid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-data-grid',
  imports: [Grid, GridExportDirective],
  template: `
    <button (click)="handleExport()">Export CSV</button>
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      [exportFeature]="{ fileName: 'employees', includeHeaders: true }"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class DataGridComponent {
  grid = injectGrid();
  rows = [];

  columns: ColumnConfig[] = [
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
    { field: 'department', header: 'Department' },
  ];

  handleExport() {
    const plugin = this.grid.element()?.getPluginByName('export');
    plugin?.exportCsv();
  }
}
```

## Demos

### Default Export

```ts
// ExportDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/export';
import '@toolbox-web/grid/features/selection';

const container = document.getElementById('export-default-demo');
if (container) {
  const sampleData = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, startDate: '2023-01-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, startDate: '2022-06-20' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, startDate: '2021-03-10' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, startDate: '2023-08-05' },
  ];
  const columns = [
    { field: 'id', header: 'ID', type: 'number' },
    { field: 'name', header: 'Name' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'number' },
    { field: 'startDate', header: 'Start Date' },
  ];

  const grid = queryGrid('tbw-grid', container)!;
  function rebuild(includeHeaders = true, onlyVisible = true, onlySelected = false) {
    grid.gridConfig = {
      columns,
      features: { selection: 'range', export: { includeHeaders, onlyVisible, onlySelected, fileName: 'grid-export' } },
    };
    grid.rows = sampleData;
  }

  rebuild();

  container.addEventListener('control-change', ((e: CustomEvent) => {
    const v = e.detail.allValues;
    rebuild(v.includeHeaders as boolean, v.onlyVisible as boolean, v.onlySelected as boolean);
  }) as EventListener);

  container.querySelector('.export-csv')?.addEventListener('click', () => grid.getPluginByName('export')?.exportCsv());
  container.querySelector('.export-excel')?.addEventListener('click', () => grid.getPluginByName('export')?.exportExcel());
  container.querySelector('.export-json')?.addEventListener('click', () => grid.getPluginByName('export')?.exportJson());
}
```

Export all visible data to CSV. Toggle "Selected only" and select some rows to export just those.

## Configuration Options

| Option           | Type      | Default   | Description                                              |
| ---------------- | --------- | --------- | -------------------------------------------------------- |
| `fileName`       | `string`  | `'export'`| Base filename (without extension)                        |
| `includeHeaders` | `boolean` | `true`    | Include column headers in export                         |
| `onlyVisible`    | `boolean` | `true`    | Export only visible columns                              |
| `onlySelected`   | `boolean` | `false`   | Export only selected rows (requires SelectionPlugin)     |

## Programmatic API

The ExportPlugin provides methods for exporting data with fine-grained control over which columns and rows to include. Like the ClipboardPlugin, it accepts `columns` and `rowIndices` parameters so you can export exactly the data you need.

### Basic Export

```ts
const exportPlugin = grid.getPluginByName('export');

// Export all visible data to CSV
exportPlugin.exportCsv();

// Export to Excel XML
exportPlugin.exportExcel();

// Export to JSON
exportPlugin.exportJson();
```

### Export with Column/Row Control (`ExportParams`)

```ts
const exportPlugin = grid.getPluginByName('export');

// Export specific columns
exportPlugin.exportCsv({
  columns: ['name', 'email', 'department'],
  fileName: 'contacts',
  includeHeaders: true,
});

// Export specific rows
exportPlugin.exportCsv({
  rowIndices: [0, 3, 7],
  fileName: 'selected-employees',
});

// Export specific columns from specific rows
exportPlugin.exportExcel({
  columns: ['name', 'salary'],
  rowIndices: [0, 1, 2],
  fileName: 'salary-report',
});
```

### `ExportParams` Reference

| Option           | Type                            | Default       | Description                         |
| ---------------- | ------------------------------- | ------------- | ----------------------------------- |
| `fileName`       | `string`                        | config value  | File name (without extension)       |
| `columns`        | `string[]`                      | -             | Specific column fields to export    |
| `rowIndices`     | `number[]`                      | -             | Specific row indices to export      |
| `includeHeaders` | `boolean`                       | config value  | Include column headers in export    |
| `processCell`    | `(value, field, row) => any`    | -             | Custom cell value processor         |
| `processHeader`  | `(header, field) => string`     | -             | Custom header processor             |
| `processHeaderRow` | `(cell, rowIndex) => HeaderRowCell \| null` | -  | Custom processor for plugin-contributed header rows (e.g. column groups). Return `null` to blank a cell; if every cell in a row is blank the row is dropped. **Since 2.10.0** |
| `mode`           | `'raw' \| 'formatted'`          | `'raw'`       | `'raw'` = underlying typed values; `'formatted'` = what the grid displays (`column.format` + type defaults applied) |
| `fileExtension`  | `string`                        | `'.xls'`      | Override file extension for Excel export (e.g. `'.xml'`) |
| `excelStyles`    | `ExcelStyleConfig`              | -             | Excel style configuration (Excel only) |

## Column Groups in Exports

**Since 2.10.0.** When the [`GroupingColumnsPlugin`](/grid/plugins/grouping-columns.md) is installed alongside the export plugin, column group headers are automatically included above the leaf headers.

How it works: the export plugin broadcasts the generic `collectHeaderRows` plugin query before emitting headers. Any plugin can reply with a `HeaderRowContribution` describing extra rows that sit above the leaf header. The grouping plugin replies with one row whose cells carry the group label and a `span` matching the number of leaf columns the group covers. The mechanism is plugin-agnostic — third-party plugins can contribute their own header rows the same way.

| Format | Behaviour |
| ------ | --------- |
| **Excel** | Each contributed row becomes a `<Row>` above the leaf headers. Cells with `span > 1` use `ss:MergeAcross="span-1"` so they appear merged in Excel. Use the new `excelStyles.groupHeaderStyle` to style group rows independently (falls back to `headerStyle`). |
| **JSON** | When at least one plugin contributes a row, output becomes `{ headerRows: HeaderRowContribution[], rows: [...] }`. Without contributions the output stays a flat array — **backward-compatible** for existing consumers. |
| **CSV** | Stays flat. CSV has no native span representation; multi-row headers would confuse most CSV consumers. |

Set `includeHeaders: false` to skip **all** headers (leaf and contributed). It's all-or-nothing — use `processHeaderRow` if you want fine-grained control over which contributed cells to emit.

```ts
// Drop all "Personal" group labels but keep "Work":
grid.getPluginByName('export').exportExcel({
  processHeaderRow: (cell) => (cell.label === 'Personal' ? null : cell),
});

// Style group rows independently of leaf headers:
grid.getPluginByName('export').exportExcel({
  excelStyles: {
    headerStyle: { font: { bold: true } },
    groupHeaderStyle: { font: { bold: true, size: 14 }, fill: { color: '#DDEEFF' } },
  },
});
```

## Custom Output / `.xlsx` Hand-off

The plugin exposes data accessors so you can drive the export pipeline without producing a download — useful for piping rows into a real OOXML writer (e.g. [ExcelJS](https://github.com/exceljs/exceljs)), copying CSV to the clipboard, sending data to a server, or pre-processing before download.

| Method                                | Returns                       | Purpose                                                          |
| ------------------------------------- | ----------------------------- | ---------------------------------------------------------------- |
| `export(params?)`                     | `Record<string, unknown>[]`   | Resolve the rows that would be exported, keyed by `column.field` |
| `formatCsv(data, params?, options?)`  | `string`                      | Format already-resolved rows as CSV (no download)                |
| `formatExcel(data, params?)`          | `string`                      | Format already-resolved rows as Excel XML (no download)          |
| `getResolvedColumns(params?)`         | `ColumnConfig[]`              | The columns (in order) an export would include                   |

All accessors honour `onlyVisible`, `onlySelected`, `columns`, and `rowIndices` (which columns/rows are included), and `processCell` (per-cell transformation — applied once on the values that get written).

`export()` additionally honours `mode` because it owns value resolution. `formatCsv()` / `formatExcel()` are pure formatters: they write whatever you pass in (after running `processCell`) and do **not** apply `mode` — use `export({ mode: 'formatted' })` upstream if you need displayed values.

```ts
import { queryGrid } from '@toolbox-web/grid';
import type { ExportPlugin } from '@toolbox-web/grid/plugins/export';

const grid = queryGrid('tbw-grid');
const exporter = grid.getPluginByName('export') as ExportPlugin;

// 1. "Export what I see" — column.format applied
exporter.exportCsv({ mode: 'formatted' });

// 2. Get the rows without producing a file
const rows = exporter.export();

// 3. Compose: copy CSV to the clipboard
const csv = exporter.formatCsv(exporter.export());
await navigator.clipboard.writeText(csv);

// 4. Hand off raw rows to a real .xlsx writer
import ExcelJS from 'exceljs';
const cols = exporter.getResolvedColumns();
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('Sheet1');
sheet.columns = cols.map((c) => ({ header: c.header ?? c.field, key: c.field }));
sheet.addRows(exporter.export());
const buffer = await workbook.xlsx.writeBuffer();
```

### `mode: 'raw' | 'formatted'`

- `'raw'` (**default**) — values straight from the row (`Date` stays `Date`, numbers stay numbers). Identical to the pre-existing `exportCsv` / `exportExcel` / `exportJson` output, so opting into the new methods is non-breaking.
- `'formatted'` — applies the column-type default formatter and `column.format(value, row)`, returning the same string the user sees in the cell. `processCell` runs last on the formatted value.

## Styled Excel Export

The export plugin produces **XML Spreadsheet 2003** output — a single-XML format natively understood by Excel, without any external dependencies.

:::note[About the file extension]
The correct extension for XML Spreadsheet 2003 is `.xml`, but most operating systems open `.xml` files in a **web browser** instead of Excel. For this reason the export defaults to `.xls`, which ensures the file opens directly in Excel.

The trade-off is that Excel shows a warning when opening the file:

> *"The file format and extension of 'filename.xls' don't match. The file could be corrupted or unsafe. Unless you trust its source, don't open it. Do you want to open it anyway?"*

Clicking **Yes** opens the file normally — the data is not corrupt.

If your users are comfortable opening `.xml` files in Excel (e.g. via right-click → "Open with"), you can set `fileExtension: '.xml'` to avoid the warning entirely:

```ts
exportPlugin.exportExcel({ fileExtension: '.xml' });
```
:::

Pass an `excelStyles` object in `ExportParams` to control header styles, per-column formatting, dynamic cell styling, column widths, and more.

```ts
// ExportStyledExcelDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/export';

const container = document.getElementById('export-styled-excel-demo');
if (container) {
  const sampleData = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, status: 'Active', startDate: '2023-01-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, status: 'Inactive', startDate: '2022-06-20' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, status: 'Active', startDate: '2021-03-10' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, status: 'Active', startDate: '2023-08-05' },
    { id: 5, name: 'Eve Martinez', department: 'Marketing', salary: 70000, status: 'Inactive', startDate: '2024-02-01' },
  ];

  const columns = [
    { field: 'id', header: 'ID', type: 'number' as const },
    { field: 'name', header: 'Name' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'number' as const },
    { field: 'status', header: 'Status' },
    { field: 'startDate', header: 'Start Date' },
  ];

  const grid = queryGrid('tbw-grid', container)!;
  grid.gridConfig = {
    columns,
    features: { export: { fileName: 'styled-export' } },
  };
  grid.rows = sampleData;

  // Plain export — no styles
  container.querySelector('.export-unstyled')?.addEventListener('click', () => {
    grid.getPluginByName('export')?.exportExcel({ fileName: 'plain-export' });
  });

  // Styled export — header, column, and conditional cell styles
  container.querySelector('.export-styled')?.addEventListener('click', () => {
    grid.getPluginByName('export')?.exportExcel({
      fileName: 'styled-report',
      excelStyles: {
        headerStyle: {
          font: { bold: true, size: 11, color: '#FFFFFF' },
          fill: { color: '#4472C4' },
          alignment: { horizontal: 'Center' },
          borders: {
            bottom: { style: 'Medium', color: '#2F5496' },
          },
        },
        defaultStyle: {
          font: { name: 'Calibri', size: 10 },
        },
        columnStyles: {
          salary: { numberFormat: '$#,##0', alignment: { horizontal: 'Right' } },
          startDate: { numberFormat: 'yyyy-mm-dd' },
        },
        cellStyle: (value, field) => {
          if (field === 'status') {
            if (value === 'Active') return { fill: { color: '#C6EFCE' }, font: { color: '#006100' } };
            if (value === 'Inactive') return { fill: { color: '#FFC7CE' }, font: { color: '#9C0006' } };
          }
          return undefined;
        },
        autoFitColumns: true,
      },
    });
  });
}
```

Click **Export Styled Excel** to download a formatted `.xls` file with bold headers, currency-formatted salaries, and color-coded status cells. Compare with **Export Plain Excel** to see the difference.

### Header & Default Styles

```ts
exportPlugin.exportExcel({
  fileName: 'report',
  excelStyles: {
    headerStyle: {
      font: { bold: true, size: 12, color: '#FFFFFF' },
      fill: { color: '#4472C4' },
      alignment: { horizontal: 'Center' },
    },
    defaultStyle: {
      font: { name: 'Calibri', size: 10 },
    },
  },
});
```

### Per-Column Styles (Financial Report)

```ts
exportPlugin.exportExcel({
  fileName: 'financial-report',
  excelStyles: {
    headerStyle: { font: { bold: true }, fill: { color: '#D9E2F3' } },
    columnStyles: {
      revenue: { numberFormat: '$#,##0.00', alignment: { horizontal: 'Right' } },
      margin: { numberFormat: '0.0%' },
      date: { numberFormat: 'yyyy-mm-dd' },
    },
    columnWidths: { name: 25, revenue: 15, margin: 10, date: 12 },
  },
});
```

### Dynamic Cell Styling (Conditional Colors)

```ts
exportPlugin.exportExcel({
  fileName: 'status-report',
  excelStyles: {
    cellStyle: (value, field) => {
      if (field === 'status') {
        if (value === 'Active') return { fill: { color: '#C6EFCE' }, font: { color: '#006100' } };
        if (value === 'Inactive') return { fill: { color: '#FFC7CE' }, font: { color: '#9C0006' } };
      }
      return undefined; // fall through to column/default style
    },
  },
});
```

### Auto-Fit Column Widths

```ts
exportPlugin.exportExcel({
  excelStyles: { autoFitColumns: true },
});
```

### `ExcelStyleConfig` Reference

| Option           | Type                                               | Default | Description                                     |
| ---------------- | -------------------------------------------------- | ------- | ----------------------------------------------- |
| `headerStyle`    | `ExcelCellStyle`                                   | -       | Style applied to all header cells               |
| `defaultStyle`   | `ExcelCellStyle`                                   | -       | Default style for all data cells                |
| `columnStyles`   | `Record<string, ExcelCellStyle>`                   | -       | Per-column style overrides (keyed by field)     |
| `cellStyle`      | `(value, field, row) => ExcelCellStyle \| undefined` | -     | Callback for per-cell dynamic styling           |
| `columnWidths`   | `Record<string, number>`                           | -       | Column widths in characters (keyed by field)    |
| `autoFitColumns` | `boolean`                                          | `false` | Auto-fit column widths from content             |

### `ExcelCellStyle` Properties

| Property       | Type     | Description                                               |
| -------------- | -------- | --------------------------------------------------------- |
| `font`         | `object` | `{ name?, size?, bold?, italic?, color? }`                |
| `fill`         | `object` | `{ color, pattern? }` — pattern defaults to `'Solid'`    |
| `numberFormat` | `string` | Excel format code (e.g. `'#,##0.00'`, `'0%'`, `'yyyy-mm-dd'`) |
| `alignment`    | `object` | `{ horizontal?, vertical?, wrapText? }`                   |
| `borders`      | `object` | `{ top?, bottom?, left?, right? }` — each `{ style, color? }` |

## Events

| Event              | Detail                                            | Description                   |
| ------------------ | ------------------------------------------------- | ----------------------------- |
| `export-complete`  | `{ format, fileName, rowCount, columnCount }`     | Fired after an export completes |

## Server-Side Considerations

The ExportPlugin exports the rows currently held in memory (`grid.rows`). It has **no async or server-side support** — it cannot fetch data it hasn't already loaded.

:::caution[Partial data with ServerSidePlugin]
When combined with the [ServerSidePlugin](/grid/plugins/server-side.md), only **cached blocks** are available for export. Rows the user hasn't scrolled to yet are placeholder objects and will not be included. If you need to export the full server dataset, fetch all rows from your API and pass them to the export method directly:

```ts
const allRows = await fetch('/api/data?all=true').then(r => r.json());
grid.rows = allRows;
grid.getPluginByName('export').exportCsv();
```
:::

## See Also

- **[Clipboard](/grid/plugins/clipboard.md)** — Copy cells to clipboard
- **[Print](/grid/plugins/print.md)** — Print the grid
- **[Server-Side Data](../server-side/)** — Block-based virtual scrolling for large datasets
