--- title: Data Grids and Tables pagination_prev: demos/frontend/index pagination_next: demos/net/index --- 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, it is easier to process an array of arrays. ### x-spreadsheet With a familiar UI, `x-spreadsheet` is an excellent choice for a modern editor. [Click here for a live integration demo.](pathname:///xspreadsheet/) **[The exposition has been moved to a separate page.](/docs/demos/grid/xs)** ### Canvas Datagrid After extensive testing, `canvas-datagrid` stood out as a high-performance grid with a straightforward API. [Click here for a live integration demo.](pathname:///cdg/index.html) **[The exposition has been moved to a separate page.](/docs/demos/grid/cdg)** ### Tabulator [Tabulator](https://tabulator.info/docs/5.4/download#xlsx) includes deep support through a special Export button. It handles the SheetJS operations internally. ### 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. The [AngularJS demo](/docs/demos/frontend/legacy#angularjs) covers more general strategies. ::: [Click here for a live integration demo.](pathname:///angularjs/ui-grid.html)
Notes (click to show) The library does not provide any way to modify the import button, so the demo includes a simple directive for a File Input HTML element. It also includes a 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.
## 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. ::: #### RDG Demo
Complete Example (click to show) 1) Create a new TypeScript `create-react-app` app: ```bash npx create-react-app sheetjs-cra --template typescript cd sheetjs-cra ``` 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 and load the data. The following screenshot was taken from the demo: ![react-data-grid screenshot](pathname:///rdg/rdg1.png)
#### 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 ( ); } ``` The most generic data representation is an array of arrays. To sate the grid, columns must be objects whose `key` property is the index converted to string: ```ts import { WorkSheet, utils } from 'xlsx'; import { textEditor, Column } from "react-data-grid"; type Row = any[]; type AOAColumn = Column; 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`: ```ts import { WorkSheet, utils } from 'xlsx'; type Row = any[]; function rdg_to_ws(rows: Row[]): WorkSheet { return utils.aoa_to_sheet(rows); } ``` :::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)); } ``` ::: ### 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
Complete Example (click to show) 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.
#### 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([]); // gdg column objects // ... return ( <> // ... // ... ); } 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 ( <> // ... // ... ); } ``` _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(0); // number of rows // ... return ( <> // ... // ... ); } 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(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(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(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 ( <> // ... // ... ); } 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 // ... ); } export default App; ``` ### 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"; export default function App() { const [rows, setRows] = useState([]); const [columns, setColumns] = useState([]); return ( ); } ``` 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[]; }; 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) => ({ // highlight-start 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 // highlight-end })); return { rows, columns }; // these can be fed to setRows / setColumns } ``` In the other direction, a worksheet can be generated with `aoa_to_sheet`: :::caution `x-data-grid` does not properly preserve row array objects, so the row arrays must be re-created. The snippet defines a `arrayify` function. ::: ```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 muidg_to_ws(rows: Row[]): WorkSheet { return utils.aoa_to_sheet(arrayify(rows)); } ``` #### MUIDG Demo
Complete Example (click to show) 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`. 3) run `npm start`. When you load the page in the browser, it will attempt to fetch and load the data.
### vue3-table-lite :::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 ::: #### VueJS Demo
Complete Example (click to show) 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: ```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 curl -L -o src/App.vue https://docs.sheetjs.com/vtl/App.vue ``` 4) run `npm run dev`. When you load the page in the browser, it will try to fetch and load the data.
#### Rows and Columns Bindings `vue3-table-lite` presents two attribute bindings: an array of column metadata (`columns`) and an array of objects representing the displayed data (`rows`). Typically both are `ref` objects: ```html ``` These can be mutated through the `value` property in VueJS lifecycle methods: ```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, columns must be objects whose `field` property is the index converted to string: ```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); } ``` ## 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
SheetJS
``` 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 ( <> // highlight-next-line {/* ... TR and TD/TH elements ... */}
); } ``` ### Material UI Table The `Table` component abstracts the `` 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(null); return ( <> // highlight-next-line
{/* ... material ui table machinations ... */}
<>); } ```
Complete Example (click to show) 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(null); return ( <> // highlight-end // highlight-next-line Dessert (100g serving) Calories Fat (g) Carbs (g) Protein (g) {rows.map((row) => ( {row.name} {row.calories} {row.fat} {row.carbs} {row.protein} ))}
// highlight-next-line ); } ``` 4) run `npm start`. Click the "Export" button and inspect the generated file.