2022-08-01 05:34:23 +00:00
|
|
|
---
|
2022-08-26 19:21:53 +00:00
|
|
|
title: Data Grids and Tables
|
2023-02-28 11:40:44 +00:00
|
|
|
pagination_prev: demos/frontend/index
|
|
|
|
pagination_next: demos/net/index
|
2022-08-01 05:34:23 +00:00
|
|
|
---
|
|
|
|
|
|
|
|
Various JavaScript UI components provide a more interactive editing experience.
|
|
|
|
Most are able to interchange with arrays of arrays or arrays of data objects.
|
|
|
|
This demo focuses on a few open source data grids.
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
[SheetJS Pro](https://sheetjs.com/pro) offers additional features like styling
|
|
|
|
and images. The UI tools typically support many of these advanced features.
|
|
|
|
|
|
|
|
To eliminate any confusion, the live examples linked from this page demonstrate
|
|
|
|
SheetJS Community Edition data interchange.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
## Managed Lifecycle
|
|
|
|
|
|
|
|
Many UI components tend to manage the entire lifecycle, providing methods to
|
|
|
|
import and export data.
|
|
|
|
|
|
|
|
The `sheet_to_json` utility function generates arrays of objects, which is
|
|
|
|
suitable for a number of libraries. When more advanced shapes are needed,
|
2022-08-25 08:22:28 +00:00
|
|
|
it is easier to process an array of arrays.
|
2022-08-01 05:34:23 +00:00
|
|
|
|
|
|
|
|
2022-08-18 08:41:34 +00:00
|
|
|
### x-spreadsheet
|
|
|
|
|
2023-02-28 11:40:44 +00:00
|
|
|
With a familiar UI, `x-spreadsheet` is an excellent choice for a modern editor.
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
[Click here for a live integration demo.](pathname:///xspreadsheet/)
|
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/xs)**
|
2022-08-18 08:41:34 +00:00
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
### Canvas Datagrid
|
2022-08-01 05:34:23 +00:00
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
After extensive testing, `canvas-datagrid` stood out as a high-performance grid
|
|
|
|
with a straightforward API.
|
2022-08-01 05:34:23 +00:00
|
|
|
|
|
|
|
[Click here for a live integration demo.](pathname:///cdg/index.html)
|
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/cdg)**
|
2022-08-24 23:48:22 +00:00
|
|
|
|
|
|
|
### Tabulator
|
|
|
|
|
2022-10-19 21:12:12 +00:00
|
|
|
[Tabulator](https://tabulator.info/docs/5.4/download#xlsx) includes deep support
|
2022-08-25 08:22:28 +00:00
|
|
|
through a special Export button. It handles the SheetJS operations internally.
|
2022-08-24 23:48:22 +00:00
|
|
|
|
|
|
|
|
2022-08-01 05:34:23 +00:00
|
|
|
### Angular UI Grid
|
|
|
|
|
|
|
|
:::warning
|
|
|
|
|
|
|
|
This UI Grid is for AngularJS, not the modern Angular. New projects should not
|
|
|
|
use AngularJS. This demo is included for legacy applications.
|
|
|
|
|
2023-01-09 17:25:32 +00:00
|
|
|
The [AngularJS demo](/docs/demos/frontend/legacy#angularjs) covers more general strategies.
|
2022-08-01 05:34:23 +00:00
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
[Click here for a live integration demo.](pathname:///angularjs/ui-grid.html)
|
|
|
|
|
|
|
|
<details><summary><b>Notes</b> (click to show)</summary>
|
|
|
|
|
|
|
|
The library does not provide any way to modify the import button, so the demo
|
2022-08-25 08:22:28 +00:00
|
|
|
includes a simple directive for a File Input HTML element. It also includes a
|
2022-08-01 05:34:23 +00:00
|
|
|
sample service for export which adds an item to the export menu.
|
|
|
|
|
|
|
|
The demo `SheetJSImportDirective` follows the prescription from the README for
|
|
|
|
File input controls using `readAsArrayBuffer`, converting to a suitable
|
|
|
|
representation and updating the scope.
|
|
|
|
|
|
|
|
`SheetJSExportService` exposes export functions for `XLSB` and `XLSX`. Other
|
|
|
|
file formats can be exported by changing the `bookType` variable. It grabs
|
|
|
|
values from the grid, builds an array of arrays, generates a workbook and forces
|
|
|
|
a download. By setting the `filename` and `sheetname` options in the `ui-grid`
|
|
|
|
options, the output can be controlled.
|
|
|
|
|
|
|
|
</details>
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
## Framework Lifecycle
|
|
|
|
|
|
|
|
For modern frameworks like React, data grids tend to follow the framework state
|
|
|
|
and idioms. The same `sheet_to_json` and `json_to_sheet` / `aoa_to_sheet`
|
|
|
|
methods are used, but they pull from a shared state object that can be mutated
|
|
|
|
with other buttons and components on the page.
|
|
|
|
|
|
|
|
### React Data Grid
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
This demo was tested against `react-data-grid 7.0.0-beta.15`, React 18.2.0,
|
|
|
|
and `create-react-app` 5.0.1 on 2022 August 16.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
2022-11-08 05:43:21 +00:00
|
|
|
<!-- spellchecker-disable -->
|
|
|
|
|
|
|
|
#### RDG Demo
|
|
|
|
|
|
|
|
<!-- spellchecker-enable -->
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
1) Create a new TypeScript `create-react-app` app:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npx create-react-app sheetjs-cra --template typescript
|
|
|
|
cd sheetjs-cra
|
|
|
|
```
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-11-08 05:43:21 +00:00
|
|
|
2) Install dependencies:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz react-data-grid
|
|
|
|
```
|
|
|
|
|
|
|
|
3) Download [`App.tsx`](pathname:///rdg/App.tsx) and replace `src/App.tsx`.
|
|
|
|
|
|
|
|
4) run `npm start`. When you load the page in the browser, it will attempt to
|
|
|
|
fetch <https://sheetjs.com/pres.numbers> and load the data.
|
|
|
|
|
|
|
|
The following screenshot was taken from the demo:
|
|
|
|
|
|
|
|
![react-data-grid screenshot](pathname:///rdg/rdg1.png)
|
|
|
|
|
|
|
|
</details>
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
#### Rows and Columns state
|
|
|
|
|
|
|
|
`react-data-grid` state consists of an Array of column metadata and an Array of
|
|
|
|
row objects. Typically both are defined in state:
|
|
|
|
|
|
|
|
```jsx
|
|
|
|
import DataGrid, { Column } from "react-data-grid";
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
const [rows, setRows] = useState([]);
|
|
|
|
const [columns, setColumns] = useState([]);
|
|
|
|
|
|
|
|
return ( <DataGrid columns={columns} rows={rows} onRowsChange={setRows} /> );
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The most generic data representation is an array of arrays. To sate the grid,
|
2022-08-25 08:22:28 +00:00
|
|
|
columns must be objects whose `key` property is the index converted to string:
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
```ts
|
|
|
|
import { WorkSheet, utils } from 'xlsx';
|
|
|
|
import { textEditor, Column } from "react-data-grid";
|
|
|
|
|
|
|
|
type Row = any[];
|
|
|
|
type AOAColumn = Column<Row>;
|
|
|
|
type RowCol = { rows: Row[]; columns: AOAColumn[]; };
|
|
|
|
|
|
|
|
function ws_to_rdg(ws: WorkSheet): RowCol {
|
|
|
|
/* create an array of arrays */
|
|
|
|
const rows = utils.sheet_to_json(ws, { header: 1 });
|
|
|
|
|
|
|
|
/* create column array */
|
|
|
|
const range = utils.decode_range(ws["!ref"]||"A1");
|
|
|
|
const columns = Array.from({ length: range.e.c + 1 }, (_, i) => ({
|
|
|
|
key: String(i), // RDG will access row["0"], row["1"], etc
|
|
|
|
name: utils.encode_col(i), // the column labels will be A, B, etc
|
|
|
|
editor: textEditor // enable cell editing
|
|
|
|
}));
|
|
|
|
|
|
|
|
return { rows, columns }; // these can be fed to setRows / setColumns
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
In the other direction, a worksheet can be generated with `aoa_to_sheet`:
|
|
|
|
|
2023-02-07 09:24:49 +00:00
|
|
|
```ts
|
|
|
|
import { WorkSheet, utils } from 'xlsx';
|
|
|
|
|
|
|
|
type Row = any[];
|
|
|
|
|
|
|
|
function rdg_to_ws(rows: Row[]): WorkSheet {
|
|
|
|
return utils.aoa_to_sheet(rows);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
:::caution
|
|
|
|
|
|
|
|
When the demo was last refreshed, row array objects were preserved. This was
|
|
|
|
not the case in a later release. The row arrays must be re-created.
|
|
|
|
|
|
|
|
The snippet defines a `arrayify` function that creates arrays if necessary.
|
|
|
|
|
|
|
|
```ts
|
|
|
|
import { WorkSheet, utils } from 'xlsx';
|
|
|
|
|
|
|
|
type Row = any[];
|
|
|
|
|
|
|
|
// highlight-start
|
|
|
|
function arrayify(rows: any[]): Row[] {
|
|
|
|
return rows.map(row => {
|
|
|
|
var length = Object.keys(row).length;
|
|
|
|
for(; length > 0; --length) if(row[length-1] != null) break;
|
|
|
|
return Array.from({length, ...row});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// highlight-end
|
|
|
|
|
|
|
|
function rdg_to_ws(rows: Row[]): WorkSheet {
|
|
|
|
return utils.aoa_to_sheet(arrayify(rows));
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2023-02-07 09:24:49 +00:00
|
|
|
:::
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
|
2023-02-07 09:24:49 +00:00
|
|
|
### Glide Data Grid
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
This demo was last tested on 2023 February 07 with the ViteJS+React+TypeScript
|
|
|
|
starter (Vite `4.1.1`, React `18.2.0`) and `@glideapps/glide-data-grid@5.2.1`.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
#### GDG Demo
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
1) Create a new project:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm create vite@latest -- sheetjs-gdg --template react-ts
|
|
|
|
cd sheetjs-gdg
|
|
|
|
npm i
|
|
|
|
```
|
|
|
|
|
|
|
|
Install SheetJS and Glide Data Grid required dependencies:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
|
|
|
|
npm i --save @glideapps/glide-data-grid lodash marked
|
|
|
|
```
|
|
|
|
|
|
|
|
Start dev server:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm run dev
|
|
|
|
```
|
|
|
|
|
|
|
|
The terminal window will display a URL (typically `http://localhost:5173`).
|
|
|
|
Open the URL with a web browser.
|
|
|
|
|
|
|
|
2) Download [`App.tsx`](pathname:///gdg/App.tsx) and replace `src/App.tsx`:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
curl -L -o src/App.tsx https://docs.sheetjs.com/gdg/App.tsx
|
|
|
|
```
|
|
|
|
|
|
|
|
Refresh the browser window and a grid should be displayed:
|
|
|
|
|
|
|
|
![glide-data-grid initial view](pathname:///gdg/pre.png)
|
|
|
|
|
|
|
|
3) To test the export functionality, make some changes to the grid data.
|
|
|
|
|
|
|
|
Suppose you believe that President Grover Cleveland should be counted once.
|
|
|
|
That would imply President Clinton should be index 41 and the indices of the
|
|
|
|
other presidents should be decremented. By double-clicking on each cell in the
|
|
|
|
Index column, a cell editor should appear. Decrement each index:
|
|
|
|
|
|
|
|
![glide-data-grid after edits](pathname:///gdg/post.png)
|
|
|
|
|
|
|
|
Click on the "Export" button to create a file! Open the file and verify.
|
|
|
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
#### Backing Store
|
|
|
|
|
|
|
|
Under the hood, the `DataEditor` component is designed to call methods and
|
|
|
|
request data to display in the grid. It is typical to store data *outside* of
|
|
|
|
component state. A `getCellContent` callback will pull data from the external
|
|
|
|
backing store, while SheetJS operations will directly act on the store:
|
|
|
|
|
|
|
|
```js
|
|
|
|
// !! THESE OBJECTS ARE DEFINED OUTSIDE OF THE COMPONENT FUNCTION !!
|
|
|
|
|
|
|
|
// this will store the raw data objects
|
|
|
|
let data: any[] = [];
|
|
|
|
// this will store the header names
|
|
|
|
let header: string[] = [];
|
|
|
|
```
|
|
|
|
|
|
|
|
#### GDG Props
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
This is a high-level overview. The official documentation should be consulted.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
_Columns_
|
|
|
|
|
|
|
|
`DataEditor` expects column metadata to be passed through a `columns` prop. This
|
|
|
|
should be managed in the component state:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { useState } from 'react';
|
|
|
|
import { DataEditor, GridColumn } from '@glideapps/glide-data-grid';
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
// highlight-next-line
|
|
|
|
const [cols, setCols] = useState<GridColumn[]>([]); // gdg column objects
|
|
|
|
// ...
|
|
|
|
return ( <>
|
|
|
|
// ...
|
|
|
|
<DataEditor
|
|
|
|
// ... props
|
|
|
|
// highlight-next-line
|
|
|
|
columns={cols}
|
|
|
|
/>
|
|
|
|
// ...
|
|
|
|
</> );
|
|
|
|
}
|
|
|
|
export default App;
|
|
|
|
```
|
|
|
|
|
|
|
|
Each `GridColumn` object expects a `title` representing the display name and an
|
|
|
|
`id` representing the key to index within the data object.
|
|
|
|
|
|
|
|
_Data_
|
|
|
|
|
|
|
|
The `DataEditor` component expects a `getCellContent` callback for supplying
|
|
|
|
data. The callback accepts column and row indices. The column index should be
|
|
|
|
used to find the header key:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { useCallback } from 'react';
|
|
|
|
import { DataEditor, GridCellKind, GridCell, Item } from '@glideapps/glide-data-grid';
|
|
|
|
|
|
|
|
// ...
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
// ...
|
|
|
|
// backing data store -> gdg
|
|
|
|
// highlight-start
|
|
|
|
const getContent = useCallback((cell: Item): GridCell => {
|
|
|
|
const [col, row] = cell;
|
|
|
|
return {
|
|
|
|
kind: GridCellKind.Text,
|
|
|
|
// header[col] is the name of the field
|
|
|
|
displayData: String(data[row]?.[header[col]]??""),
|
|
|
|
data: data[row]?.[header[col]],
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
// highlight-end
|
|
|
|
// ...
|
|
|
|
return ( <>
|
|
|
|
// ...
|
|
|
|
<DataEditor
|
|
|
|
// ... props
|
|
|
|
// highlight-next-line
|
|
|
|
getCellContent={getContent}
|
|
|
|
/>
|
|
|
|
// ...
|
|
|
|
</> );
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
_Row Count_
|
|
|
|
|
|
|
|
`DataEditor` also accepts a `rows` property indicating the number of rows. This
|
|
|
|
is best managed in state:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { useState } from 'react';
|
|
|
|
import { DataEditor } from '@glideapps/glide-data-grid';
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
// highlight-next-line
|
|
|
|
const [rows, setRows] = useState<number>(0); // number of rows
|
|
|
|
// ...
|
|
|
|
return ( <>
|
|
|
|
// ...
|
|
|
|
<DataEditor
|
|
|
|
// ... props
|
|
|
|
// highlight-next-line
|
|
|
|
rows={rows}
|
|
|
|
/>
|
|
|
|
// ...
|
|
|
|
</> );
|
|
|
|
}
|
|
|
|
export default App;
|
|
|
|
```
|
|
|
|
|
|
|
|
_Editing Data_
|
|
|
|
|
|
|
|
The demo uses the `onCellEdited` callback to write back to the data store.
|
|
|
|
|
|
|
|
#### Parsing Data
|
|
|
|
|
|
|
|
_SheetJS to Data Store_
|
|
|
|
|
|
|
|
The raw data objects are readily generated with `sheet_to_json`. The headers
|
|
|
|
can be pulled by extracting the first row of the worksheet:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { utils, WorkBook } from 'xlsx';
|
|
|
|
|
|
|
|
// ...
|
|
|
|
|
|
|
|
const update_backing_store = (wb: WorkBook) => {
|
|
|
|
// get first worksheet
|
|
|
|
const sheet = wb.Sheets[wb.SheetNames[0]];
|
|
|
|
|
|
|
|
// set data
|
|
|
|
// highlight-next-line
|
|
|
|
data = utils.sheet_to_json<any>(sheet);
|
|
|
|
|
|
|
|
// create a range consisting of the first row
|
|
|
|
const range = utils.decode_range(sheet["!ref"]??"A1"); // original range
|
|
|
|
range.e.r = range.s.r; // set ending row to starting row (select first row)
|
|
|
|
|
|
|
|
// pull headers
|
|
|
|
// highlight-next-line
|
|
|
|
header = utils.sheet_to_json<string[]>(sheet, {header: 1, range})[0];
|
|
|
|
};
|
|
|
|
|
|
|
|
// ...
|
|
|
|
```
|
|
|
|
|
|
|
|
_Data Store to GDG_
|
|
|
|
|
|
|
|
Scheduling a refresh for the `DataEditor` involves updating the grid column
|
|
|
|
metadata and row count through the standard state. It also requires a special
|
|
|
|
`updateCells` call to instruct the grid to mark the cached data as stale:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { useRef } from 'react'
|
|
|
|
import { WorkBook } from 'xlsx'
|
|
|
|
import { DataEditor, GridColumn, Item, DataEditorRef } from '@glideapps/glide-data-grid'
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
const ref = useRef<DataEditorRef>(null); // gdg ref
|
|
|
|
// ...
|
|
|
|
const parse_wb = (wb: WorkBook) => {
|
|
|
|
update_backing_store(wb);
|
|
|
|
|
|
|
|
// highlight-start
|
|
|
|
// update column metadata by pulling from external header keys
|
|
|
|
setCols(header.map(h => ({title: h, id: h} as GridColumn)));
|
|
|
|
|
|
|
|
// update number of rows
|
|
|
|
setRows(data.length);
|
|
|
|
|
|
|
|
if(data.length > 0) {
|
|
|
|
// create an array of the cells that must be updated
|
|
|
|
let cells = data.map(
|
|
|
|
(_,R) => Array.from({length:header.length}, (_,C) => ({cell: ([C,R] as Item)}))
|
|
|
|
).flat();
|
|
|
|
// initiate update using the `ref` attached to the DataEditor
|
|
|
|
ref.current?.updateCells(cells)
|
|
|
|
}
|
|
|
|
// highlight-end
|
|
|
|
};
|
|
|
|
// ...
|
|
|
|
return ( <>
|
|
|
|
// ...
|
|
|
|
<DataEditor
|
|
|
|
// ... props
|
|
|
|
// highlight-next-line
|
|
|
|
ref={ref}
|
|
|
|
/>
|
|
|
|
// ...
|
|
|
|
</> );
|
|
|
|
}
|
|
|
|
export default App;
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Writing Data
|
|
|
|
|
|
|
|
`json_to_sheet` works directly on the `data` array:
|
|
|
|
|
|
|
|
```js
|
|
|
|
const ws = utils.json_to_sheet(data); // easy :)
|
|
|
|
```
|
|
|
|
|
|
|
|
Since the editor can change the header titles, it is strongly recommended to
|
|
|
|
pull column data from the state and rewrite the header row:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { utils, writeFileXLSX } from 'xlsx';
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
// ...
|
|
|
|
const exportXLSX = useCallback(() => {
|
|
|
|
// highlight-start
|
|
|
|
// generate worksheet using data with the order specified in the columns array
|
|
|
|
const ws = utils.json_to_sheet(data, {header: cols.map(c => c.id ?? c.title)});
|
|
|
|
|
|
|
|
// rewrite header row with titles
|
|
|
|
utils.sheet_add_aoa(ws, [cols.map(c => c.title ?? c.id)], {origin: "A1"});
|
|
|
|
// highlight-end
|
|
|
|
|
|
|
|
// create workbook
|
|
|
|
const wb = utils.book_new();
|
|
|
|
utils.book_append_sheet(wb, ws, "Export"); // replace with sheet name
|
|
|
|
// download file
|
|
|
|
writeFileXLSX(wb, "sheetjs-gdg.xlsx");
|
|
|
|
}, []);
|
|
|
|
// ...
|
|
|
|
return ( <>
|
|
|
|
// ...
|
|
|
|
// highlight-next-line
|
|
|
|
<button onClick={exportXLSX}><b>Export XLSX!</b></button>
|
|
|
|
// ...
|
|
|
|
</> );
|
2022-08-17 07:10:01 +00:00
|
|
|
}
|
2023-02-07 09:24:49 +00:00
|
|
|
export default App;
|
2022-08-17 07:10:01 +00:00
|
|
|
```
|
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
### Material UI Data Grid
|
|
|
|
|
|
|
|
Material UI Data Grid and React Data Grid share many state patterns and idioms.
|
|
|
|
Differences from ["React Data Grid"](#react-data-grid) will be highlighted.
|
|
|
|
|
|
|
|
[A complete example is included below.](#muidg-demo)
|
|
|
|
|
|
|
|
:::warning
|
|
|
|
|
|
|
|
Despite presenting an editable UI, Material UI Data Grid version `5.17.0` does
|
|
|
|
not update the state when values are changed. The demo uses the React Data Grid
|
|
|
|
editable structure in the hopes that a future version does support state.
|
|
|
|
|
|
|
|
Until the issues are resolved, "React Data Grid" is an excellent choice.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
**Rows and Columns State**
|
|
|
|
|
|
|
|
The analogue of `Column` is `GridColDef`. The simple structure looks like:
|
|
|
|
|
|
|
|
```js
|
|
|
|
// highlight-next-line
|
|
|
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
export default function App() {
|
2022-09-03 10:02:45 +00:00
|
|
|
const [rows, setRows] = useState([]);
|
|
|
|
const [columns, setColumns] = useState([]);
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
return ( <DataGrid columns={columns} rows={rows} onRowsChange={setRows} /> );
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The most generic data representation is an array of arrays. To sate the grid,
|
|
|
|
columns must be objects whose `field` property is the index converted to string:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
import { WorkSheet, utils } from 'xlsx';
|
|
|
|
// highlight-next-line
|
|
|
|
import { GridColDef } from "@mui/x-data-grid";
|
|
|
|
|
|
|
|
type Row = any[];
|
|
|
|
type RowCol = { rows: Row[]; columns: GridColDef[]; };
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
function ws_to_muidg(ws: WorkSheet): RowCol {
|
|
|
|
/* create an array of arrays */
|
|
|
|
const rows = utils.sheet_to_json(ws, { header: 1 });
|
|
|
|
|
|
|
|
/* create column array */
|
|
|
|
const range = utils.decode_range(ws["!ref"]||"A1");
|
|
|
|
const columns = Array.from({ length: range.e.c + 1 }, (_, i) => ({
|
2022-08-17 07:10:01 +00:00
|
|
|
// highlight-start
|
2022-09-03 10:02:45 +00:00
|
|
|
field: String(i), // MUIDG will access row["0"], row["1"], etc
|
|
|
|
headerName: utils.encode_col(i), // the column labels will be A, B, etc
|
|
|
|
editable: true // enable cell editing
|
2022-08-17 07:10:01 +00:00
|
|
|
// highlight-end
|
2022-09-03 10:02:45 +00:00
|
|
|
}));
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
return { rows, columns }; // these can be fed to setRows / setColumns
|
|
|
|
}
|
|
|
|
```
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
In the other direction, a worksheet can be generated with `aoa_to_sheet`:
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
:::caution
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
`x-data-grid` does not properly preserve row array objects, so the row arrays
|
|
|
|
must be re-created. The snippet defines a `arrayify` function.
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
:::
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
```ts
|
|
|
|
import { WorkSheet, utils } from 'xlsx';
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
type Row = any[];
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
// highlight-start
|
|
|
|
function arrayify(rows: any[]): Row[] {
|
|
|
|
return rows.map(row => {
|
|
|
|
var length = Object.keys(row).length;
|
|
|
|
for(; length > 0; --length) if(row[length-1] != null) break;
|
|
|
|
return Array.from({length, ...row});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// highlight-end
|
|
|
|
|
|
|
|
function muidg_to_ws(rows: Row[]): WorkSheet {
|
|
|
|
return utils.aoa_to_sheet(arrayify(rows));
|
2022-08-17 07:10:01 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
<!-- spellchecker-disable -->
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
#### MUIDG Demo
|
|
|
|
|
|
|
|
<!-- spellchecker-enable -->
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
0) [Follow the React Data Grid demo](#rdg-demo) and generate the sample app.
|
|
|
|
|
|
|
|
1) Install dependencies:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz @mui/x-data-grid @emotion/react @emotion/styled
|
|
|
|
```
|
|
|
|
|
|
|
|
2) Download [`App.tsx`](pathname:///muidg/App.tsx) and replace `src/App.tsx`.
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
3) run `npm start`. When you load the page in the browser, it will attempt to
|
|
|
|
fetch <https://sheetjs.com/pres.numbers> and load the data.
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
</details>
|
|
|
|
|
2022-08-25 08:22:28 +00:00
|
|
|
<!-- spellchecker-disable -->
|
|
|
|
|
2022-08-18 08:41:34 +00:00
|
|
|
### vue3-table-lite
|
|
|
|
|
2022-08-25 08:22:28 +00:00
|
|
|
<!-- spellchecker-enable -->
|
|
|
|
|
2022-08-18 08:41:34 +00:00
|
|
|
:::note
|
|
|
|
|
|
|
|
This demo was tested against `vue3-table-lite 1.2.4`, VueJS `3.2.37`, ViteJS
|
|
|
|
3.0.7, and `@vitejs/plugin-vue` 3.0.3 on 2022 August 18
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
2022-11-08 05:43:21 +00:00
|
|
|
#### VueJS Demo
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
1) Create a new ViteJS App using the VueJS + TypeScript template:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm create vite@latest sheetjs-vue -- --template vue-ts
|
|
|
|
cd sheetjs-vue
|
|
|
|
```
|
|
|
|
|
|
|
|
2) Install dependencies:
|
2022-08-18 08:41:34 +00:00
|
|
|
|
2022-11-08 05:43:21 +00:00
|
|
|
```bash
|
|
|
|
npm i
|
|
|
|
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz vue3-table-lite
|
|
|
|
```
|
|
|
|
|
|
|
|
3) Download [`src/App.vue`](pathname:///vtl/App.vue) and replace the contents:
|
|
|
|
|
|
|
|
```bash
|
2023-01-10 00:31:37 +00:00
|
|
|
curl -L -o src/App.vue https://docs.sheetjs.com/vtl/App.vue
|
2022-11-08 05:43:21 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
4) run `npm run dev`. When you load the page in the browser, it will try to
|
|
|
|
fetch <https://sheetjs.com/pres.numbers> and load the data.
|
|
|
|
|
|
|
|
</details>
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
#### Rows and Columns Bindings
|
|
|
|
|
2022-08-25 08:22:28 +00:00
|
|
|
`vue3-table-lite` presents two attribute bindings: an array of column metadata
|
2022-08-18 08:41:34 +00:00
|
|
|
(`columns`) and an array of objects representing the displayed data (`rows`).
|
|
|
|
Typically both are `ref` objects:
|
|
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
<script setup lang="ts">
|
|
|
|
import { ref } from "vue";
|
|
|
|
import VueTableLite from "vue3-table-lite/ts";
|
|
|
|
|
|
|
|
/* rows */
|
|
|
|
type Row = any[];
|
|
|
|
const rows = ref<Row[]>([]);
|
|
|
|
|
|
|
|
/* columns */
|
|
|
|
type Column = { field: string; label: string; };
|
|
|
|
const columns = ref<Column[]>([]);
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<vue-table-lite :columns="columns" :rows="rows"></vue-table-lite>
|
|
|
|
</template>
|
|
|
|
```
|
|
|
|
|
2022-08-25 08:22:28 +00:00
|
|
|
These can be mutated through the `value` property in VueJS lifecycle methods:
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
```ts
|
|
|
|
import { onMounted } from "vue";
|
|
|
|
onMounted(() => {
|
|
|
|
columns.value = [ { field: "name", label: "Names" }];
|
|
|
|
rows.value = [ { name: "SheetJS" }, { name: "VueJS" } ];
|
|
|
|
})
|
|
|
|
```
|
|
|
|
|
|
|
|
The most generic data representation is an array of arrays. To sate the grid,
|
2022-08-25 08:22:28 +00:00
|
|
|
columns must be objects whose `field` property is the index converted to string:
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
```js
|
|
|
|
import { ref } from "vue";
|
|
|
|
import { utils } from 'xlsx';
|
|
|
|
|
|
|
|
/* generate row and column data */
|
|
|
|
function ws_to_vte(ws) {
|
|
|
|
/* create an array of arrays */
|
|
|
|
const rows = utils.sheet_to_json(ws, { header: 1 });
|
|
|
|
|
|
|
|
/* create column array */
|
|
|
|
const range = utils.decode_range(ws["!ref"]||"A1");
|
|
|
|
const columns = Array.from({ length: range.e.c + 1 }, (_, i) => ({
|
|
|
|
field: String(i), // VTE will access row["0"], row["1"], etc
|
|
|
|
label: utils.encode_col(i), // the column labels will be A, B, etc
|
|
|
|
}));
|
|
|
|
|
|
|
|
return { rows, columns };
|
|
|
|
}
|
|
|
|
|
|
|
|
const rows = ref([]);
|
|
|
|
const columns = ref([]);
|
|
|
|
|
|
|
|
/* update refs */
|
|
|
|
function update_refs(ws) {
|
|
|
|
const data = ws_to_vte(ws);
|
|
|
|
rows.value = data.rows;
|
|
|
|
columns.value = data.columns;
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
In the other direction, a worksheet can be generated with `aoa_to_sheet`:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { utils } from 'xlsx';
|
|
|
|
|
|
|
|
const rows = ref([]);
|
|
|
|
|
|
|
|
function vte_to_ws(rows) {
|
|
|
|
return utils.aoa_to_sheet(rows.value);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
## Standard HTML Tables
|
|
|
|
|
|
|
|
Many UI components present styled HTML tables. Data can be extracted from the
|
|
|
|
tables given a reference to the underlying TABLE element:
|
|
|
|
|
|
|
|
```js
|
|
|
|
function export_html_table(table) {
|
|
|
|
const wb = XLSX.utils.table_to_book(table);
|
|
|
|
XLSX.writeFile(wb, "HTMLTable.xlsx");
|
|
|
|
} // yes, it's that easy!
|
|
|
|
```
|
|
|
|
|
|
|
|
:::info
|
|
|
|
|
|
|
|
SheetJS CE is focused on data preservation and will extract values from tables.
|
|
|
|
|
|
|
|
[SheetJS Pro](https://sheetjs.com/pro) offers styling support when reading from
|
|
|
|
TABLE elements and when writing to XLSX and other spreadsheet formats.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
### Fixed Tables
|
|
|
|
|
|
|
|
When the page has a raw HTML table, the easiest solution is to attach an `id`:
|
|
|
|
|
|
|
|
```html
|
|
|
|
<table id="xport"><tr><td>SheetJS</td></tr></table>
|
|
|
|
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/shim.min.js"></script>
|
|
|
|
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
|
|
|
|
<script>
|
|
|
|
/* as long as this script appears after the table, it will be visible */
|
|
|
|
var tbl = document.getElementById("xport");
|
|
|
|
const wb = XLSX.utils.table_to_book(tbl);
|
|
|
|
XLSX.writeFile(wb, "HTMLTable.xlsx");
|
|
|
|
</script>
|
|
|
|
```
|
|
|
|
|
|
|
|
When programmatically constructing the table in the browser, retain a reference:
|
|
|
|
|
|
|
|
```js
|
|
|
|
var tbl = document.createElement("TABLE");
|
|
|
|
tbl.insertRow(0).insertCell(0).innerHTML = "SheetJS";
|
|
|
|
document.body.appendChild(tbl);
|
|
|
|
const wb = XLSX.utils.table_to_book(tbl);
|
|
|
|
XLSX.writeFile(wb, "HTMLFlicker.xlsx");
|
|
|
|
document.body.removeChild(tbl);
|
|
|
|
```
|
|
|
|
|
|
|
|
### React
|
|
|
|
|
|
|
|
The typical solution is to attach a Ref to the table element. The `current`
|
|
|
|
property will be a live reference which plays nice with `table_to_book`:
|
|
|
|
|
|
|
|
```jsx
|
|
|
|
// highlight-next-line
|
|
|
|
import { useRef } from "react";
|
|
|
|
|
|
|
|
export default function ReactTable() {
|
|
|
|
// highlight-next-line
|
|
|
|
const tbl = useRef(null);
|
|
|
|
|
|
|
|
return ( <>
|
|
|
|
<button onClick={() => {
|
|
|
|
// highlight-next-line
|
|
|
|
const wb = XLSX.utils.table_to_book(tbl.current);
|
|
|
|
XLSX.writeFile(wb, "ReactTable.xlsx");
|
|
|
|
}}>Export</button>
|
|
|
|
// highlight-next-line
|
|
|
|
<table ref={tbl}>
|
|
|
|
{/* ... TR and TD/TH elements ... */}
|
|
|
|
</table>
|
2023-02-28 11:40:44 +00:00
|
|
|
</> );
|
2022-09-03 10:02:45 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Material UI Table
|
|
|
|
|
|
|
|
The `Table` component abstracts the `<table>` element in HTML.
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
import TableContainer from '@mui/material/TableContainer';
|
|
|
|
import Table from '@mui/material/Table';
|
|
|
|
// ...
|
|
|
|
// highlight-next-line
|
|
|
|
import { useRef } from "react";
|
|
|
|
|
|
|
|
// ...
|
|
|
|
export default function BasicTable() {
|
|
|
|
// highlight-next-line
|
|
|
|
const tbl = useRef<HTMLTableElement>(null);
|
2023-02-28 11:40:44 +00:00
|
|
|
return ( <>
|
2022-09-03 10:02:45 +00:00
|
|
|
<button onClick={() => {
|
|
|
|
const wb = utils.table_to_book(tbl.current);
|
|
|
|
writeFileXLSX(wb, "SheetJSMaterialUI.xlsx");
|
|
|
|
}}>Export</button>
|
|
|
|
<TableContainer {...}>
|
|
|
|
// highlight-next-line
|
|
|
|
<Table {...} ref={tbl}>
|
|
|
|
{/* ... material ui table machinations ... */}
|
|
|
|
</Table>
|
|
|
|
</TableContainer>
|
|
|
|
<>);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
1) Create a new TypeScript `create-react-app` app:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npx create-react-app sheetjs-mui --template typescript
|
|
|
|
cd sheetjs-mui
|
|
|
|
```
|
|
|
|
|
|
|
|
2) Install dependencies:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz @mui/material
|
|
|
|
```
|
|
|
|
|
|
|
|
3) Replace `src/App.tsx` with the following code. This is based on the official
|
|
|
|
Material UI Table example. Differences are highlighted.
|
|
|
|
|
|
|
|
```tsx title="src/App.tsx"
|
|
|
|
// highlight-start
|
|
|
|
import React, { useEffect, useState, useRef, ChangeEvent } from "react";
|
|
|
|
import { utils, writeFileXLSX } from 'xlsx';
|
|
|
|
// highlight-end
|
|
|
|
import Table from '@mui/material/Table';
|
|
|
|
import TableBody from '@mui/material/TableBody';
|
|
|
|
import TableCell from '@mui/material/TableCell';
|
|
|
|
import TableContainer from '@mui/material/TableContainer';
|
|
|
|
import TableHead from '@mui/material/TableHead';
|
|
|
|
import TableRow from '@mui/material/TableRow';
|
|
|
|
import Paper from '@mui/material/Paper';
|
|
|
|
|
|
|
|
function createData(
|
|
|
|
name: string,
|
|
|
|
calories: number,
|
|
|
|
fat: number,
|
|
|
|
carbs: number,
|
|
|
|
protein: number,
|
|
|
|
) {
|
|
|
|
return { name, calories, fat, carbs, protein };
|
|
|
|
}
|
|
|
|
|
|
|
|
const rows = [
|
|
|
|
createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
|
|
|
|
createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
|
|
|
|
createData('Eclair', 262, 16.0, 24, 6.0),
|
|
|
|
createData('Cupcake', 305, 3.7, 67, 4.3),
|
|
|
|
createData('Gingerbread', 356, 16.0, 49, 3.9),
|
|
|
|
];
|
|
|
|
|
|
|
|
export default function BasicTable() {
|
|
|
|
// highlight-start
|
|
|
|
const tbl = useRef<HTMLTableElement>(null);
|
|
|
|
return ( <>
|
|
|
|
<button onClick={() => {
|
|
|
|
const wb = utils.table_to_book(tbl.current);
|
|
|
|
writeFileXLSX(wb, "SheetJSMaterialUI.xlsx");
|
|
|
|
}}>Export</button>
|
|
|
|
// highlight-end
|
|
|
|
<TableContainer component={Paper}>
|
|
|
|
// highlight-next-line
|
|
|
|
<Table sx={{ minWidth: 650 }} aria-label="simple table" ref={tbl}>
|
|
|
|
<TableHead>
|
|
|
|
<TableRow>
|
|
|
|
<TableCell>Dessert (100g serving)</TableCell>
|
|
|
|
<TableCell align="right">Calories</TableCell>
|
|
|
|
<TableCell align="right">Fat (g)</TableCell>
|
|
|
|
<TableCell align="right">Carbs (g)</TableCell>
|
|
|
|
<TableCell align="right">Protein (g)</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
</TableHead>
|
|
|
|
<TableBody>
|
|
|
|
{rows.map((row) => (
|
|
|
|
<TableRow
|
|
|
|
key={row.name}
|
|
|
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
|
|
|
>
|
|
|
|
<TableCell component="th" scope="row">
|
|
|
|
{row.name}
|
|
|
|
</TableCell>
|
|
|
|
<TableCell align="right">{row.calories}</TableCell>
|
|
|
|
<TableCell align="right">{row.fat}</TableCell>
|
|
|
|
<TableCell align="right">{row.carbs}</TableCell>
|
|
|
|
<TableCell align="right">{row.protein}</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
))}
|
|
|
|
</TableBody>
|
|
|
|
</Table>
|
|
|
|
</TableContainer>
|
|
|
|
// highlight-next-line
|
|
|
|
</> );
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
4) run `npm start`. Click the "Export" button and inspect the generated file.
|
|
|
|
|
|
|
|
</details>
|