From 9ae03ac35a7a8506f7a242662f9cd265ae978fe5 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Sat, 3 Sep 2022 06:02:45 -0400 Subject: [PATCH] docusaurus --- .spelling | 1 + Makefile | 2 +- docz/docs/03-demos/04-grid.md | 460 +++++++++++++++++++++------- docz/docs/03-demos/11-angular.md | 2 +- docz/docs/03-demos/41-nosql.md | 101 ++++++ docz/docs/03-demos/index.md | 2 +- docz/package.json | 12 +- docz/static/muidg/App.tsx | 111 +++++++ docz/static/pouchdb/master.zip | Bin 0 -> 8362 bytes docz/static/rdg/App.tsx | 110 +++++++ docz/static/{react => rdg}/rdg1.png | Bin 11 files changed, 679 insertions(+), 122 deletions(-) create mode 100644 docz/static/muidg/App.tsx create mode 100644 docz/static/pouchdb/master.zip create mode 100644 docz/static/rdg/App.tsx rename docz/static/{react => rdg}/rdg1.png (100%) diff --git a/.spelling b/.spelling index 14d7ffea..b578e6f0 100644 --- a/.spelling +++ b/.spelling @@ -197,6 +197,7 @@ PPI PhantomJS Photoshop PostgreSQL +PouchDB PowerShell Preact QuickJS diff --git a/Makefile b/Makefile index cd0407ef..9dee755f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ build: .PHONY: init init: - cd docz; npm i; cd .. + cd docz; npm i || npm i --legacy-peer-deps; cd .. .PHONY: dev dev: diff --git a/docz/docs/03-demos/04-grid.md b/docz/docs/03-demos/04-grid.md index 443acec0..470030c0 100644 --- a/docz/docs/03-demos/04-grid.md +++ b/docz/docs/03-demos/04-grid.md @@ -303,6 +303,35 @@ function ws_to_rdg(ws: WorkSheet): RowCol { In the other direction, a worksheet can be generated with `aoa_to_sheet`: +:::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)); +} +``` + ```ts import { WorkSheet, utils } from 'xlsx'; @@ -334,125 +363,128 @@ cd sheetjs-cra npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz react-data-grid ``` -3) Replace the contents of `src/App.tsx` with the following code. Note: a copy -to clipboard button will show up if you move your mouse over the code. The -notable SheetJS integration code is highlighted below: - -```tsx title="src/App.tsx" -import React, { useEffect, useState, ChangeEvent } from "react"; -import DataGrid, { textEditor, Column } from "react-data-grid"; -import { read, utils, WorkSheet, writeFile } from "xlsx"; - -import './App.css'; - -type DataSet = { [index: string]: WorkSheet; }; -type Row = any[]; -type AOAColumn = Column; -type RowCol = { rows: Row[]; columns: AOAColumn[]; }; - -/* this method returns `rows` and `columns` data for sheet change */ -const getRowsCols = ( data: DataSet, sheetName: string ): RowCol => ({ - rows: utils.sheet_to_json(data[sheetName], {header:1}), - columns: Array.from({ - length: utils.decode_range(data[sheetName]["!ref"]||"A1").e.c + 1 - }, (_, i) => ({ key: String(i), name: utils.encode_col(i), editor: textEditor })) -}); - -export default function App() { - const [rows, setRows] = useState([]); // data rows - const [columns, setColumns] = useState([]); // columns - const [workBook, setWorkBook] = useState({} as DataSet); // workbook - const [sheets, setSheets] = useState([]); // list of sheet names - const [current, setCurrent] = useState(""); // selected sheet - - /* called when sheet dropdown is changed */ - function selectSheet(name: string) { - // highlight-start - /* update workbook cache in case the current worksheet was changed */ - workBook[current] = utils.aoa_to_sheet(rows); - // highlight-end - - /* get data for desired sheet and update state */ - const { rows: new_rows, columns: new_columns } = getRowsCols(workBook, name); - setRows(new_rows); - setColumns(new_columns); - setCurrent(name); - } - - /* this method handles refreshing the state with new workbook data */ - async function handleAB(file: ArrayBuffer): Promise { - // highlight-start - /* read file data */ - const data = read(file); - // highlight-end - - /* update workbook state */ - setWorkBook(data.Sheets); - setSheets(data.SheetNames); - - /* select the first worksheet */ - const name = data.SheetNames[0]; - const { rows: new_rows, columns: new_columns } = getRowsCols(data.Sheets, name); - setRows(new_rows); - setColumns(new_columns); - setCurrent(name); - } - - /* called when file input element is used to select a new file */ - async function handleFile(ev: ChangeEvent): Promise { - const file = await ev.target.files?.[0]?.arrayBuffer(); - if(file) await handleAB(file); - } - - /* when page is loaded, fetch and processs worksheet */ - useEffect(() => { (async () => { - const f = await fetch("https://sheetjs.com/pres.numbers"); - await handleAB(await f.arrayBuffer()); - })(); }, []); - - /* method is called when one of the save buttons is clicked */ - function saveFile(ext: string): void { - /* update current worksheet in case changes were made */ - workBook[current] = utils.aoa_to_sheet(rows); - - // highlight-start - /* construct workbook and loop through worksheets */ - const wb = utils.book_new(); - sheets.forEach((n) => { utils.book_append_sheet(wb, workBook[n], n); }); - // highlight-end - - /* generate a file and download it */ - writeFile(wb, "sheet." + ext); - } - - return ( - <> -

SheetJS × React-Data-Grid Demo

- - {sheets.length > 0 && ( <> -

Use the dropdown to switch to a worksheet:  - -

-
Current Sheet: {current}
- -

Click one of the buttons to create a new file with the modified data

-
{["xlsx", "xlsb", "xls"].map((ext) => ( - - ))}
- )} - - ); -} -``` +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:///react/rdg1.png) +![react-data-grid screenshot](pathname:///rdg/rdg1.png) + + + +### 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.
@@ -585,3 +617,205 @@ cd .. fetch and load the data. + +## 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. + +
diff --git a/docz/docs/03-demos/11-angular.md b/docz/docs/03-demos/11-angular.md index 352aa5f5..a7668ba2 100644 --- a/docz/docs/03-demos/11-angular.md +++ b/docz/docs/03-demos/11-angular.md @@ -15,7 +15,7 @@ and TypeScript familiarity is assumed. Other demos cover general Angular deployments, including: - [iOS and Android applications powered by NativeScript](./mobile#nativescript) -- [iOS and Android applications powered by ionic](./mobile#ionic) +- [iOS and Android applications powered by Ionic](./mobile#ionic) :::warning diff --git a/docz/docs/03-demos/41-nosql.md b/docz/docs/03-demos/41-nosql.md index 30ecf523..f47bddb8 100644 --- a/docz/docs/03-demos/41-nosql.md +++ b/docz/docs/03-demos/41-nosql.md @@ -205,3 +205,104 @@ Inspect the output and compare with the data in `SheetJSRedisTest.mjs`. Open `SheetJSRedis.xlsx` and verify the columns have the correct data + + +### PouchDB + +`Database#allDocs` is the standard approach for bulk data export. The generated +row objects have an additional `_id` and `_rev` keys that should be removed. + +Nested objects must be flattened. The ["Tutorial"](../getting-started/example) +includes an example of constructing a simple array. + +```js +function export_pouchdb_to_xlsx(db) { + /* fetch all rows, including the underlying data */ + db.allDocs({include_docs: true}, function(err, doc) { + + /* pull the individual data rows */ + const aoo = doc.rows.map(r => { + /* `rest` will include every field from `r` except for _id and _rev */ + const { _id, _rev, ...rest } = r; + return rest; + }); + + /* generate worksheet */ + const ws = XLSX.utils.json_to_sheet(aoo); + + /* generate workbook and export */ + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "Sheet1"); + XLSX.writeFile(wb, "SheetJSPouch.xlsx"); + }); +} +``` + +
Complete Example (click to show) + +0) Download the "Working Version" from the Getting Started guide. + +[ZIP](https://github.com/nickcolley/getting-started-todo/archive/master.zip) + +The ZIP file should have `MD5` checksum `ac4da7cb0cade1be293ba222462f109c`: + +```bash +curl -LO https://github.com/nickcolley/getting-started-todo/archive/master.zip +md5sum master.zip || md5 master.zip +### the checksum will be printed +``` + +If the download is unavailable, a mirror is available at + + +1) Unzip the `master.zip` file and enter the folder: + +```bash +unzip master.zip +cd getting-started-todo-master +``` + +2) Edit `index.html` to reference the SheetJS library and add a button: + +```html title="index.html" + + + + + +
+``` + +3) Just before the end of `app.js`, add a `click` event listener: + +```js title="app.js" + if (remoteCouch) { + sync(); + } + + // highlight-start + document.getElementById("xport").addEventListener("click", function() { + db.allDocs({include_docs: true}, function(err, doc) { + const aoo = doc.rows.map(r => { + const { _id, _rev, ... rest } = r.doc; + return rest; + }); + const ws = XLSX.utils.json_to_sheet(aoo); + const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Sheet1"); + XLSX.writeFile(wb, "SheetJSPouch.xlsx"); + }); + }); + // highlight-end +})(); +``` + +4) Start a local web server: + +```bash +npx http-server . +``` + +Access `http://localhost:8080` from your browser. Add a few items and click +the "Export!" button to generate a new file. + +
diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index d34e8375..15763544 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -35,7 +35,7 @@ run in the web browser, demos will include interactive examples. - [`react-data-grid`](./grid#react-data-grid) - [`vue3-table-lite`](./grid#vue3-table-lite) - [`angular-ui-grid`](./grid#angular-ui-grid) - +- [`material ui`](./grid#material-ui-table) ### Platforms and Integrations - [`Command-Line Tools`](./cli) diff --git a/docz/package.json b/docz/package.json index 288efe60..8a23db25 100644 --- a/docz/package.json +++ b/docz/package.json @@ -15,11 +15,11 @@ }, "dependencies": { "@cmfcmf/docusaurus-search-local": "0.11.0", - "@docusaurus/core": "2.0.1", - "@docusaurus/plugin-client-redirects": "2.0.1", - "@docusaurus/preset-classic": "2.0.1", - "@docusaurus/theme-common": "2.0.1", - "@docusaurus/theme-live-codeblock": "2.0.1", + "@docusaurus/core": "2.1.0", + "@docusaurus/plugin-client-redirects": "2.1.0", + "@docusaurus/preset-classic": "2.1.0", + "@docusaurus/theme-common": "2.1.0", + "@docusaurus/theme-live-codeblock": "2.1.0", "@mdx-js/react": "1.6.22", "clsx": "1.2.1", "prism-react-renderer": "1.3.5", @@ -28,7 +28,7 @@ "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz" }, "devDependencies": { - "@docusaurus/module-type-aliases": "2.0.1" + "@docusaurus/module-type-aliases": "2.1.0" }, "browserslist": { "production": [ diff --git a/docz/static/muidg/App.tsx b/docz/static/muidg/App.tsx new file mode 100644 index 00000000..dbc388c9 --- /dev/null +++ b/docz/static/muidg/App.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState, ChangeEvent } from "react"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { read, utils, WorkSheet, writeFile } from "xlsx"; + +import './App.css'; + +type DataSet = { [index: string]: WorkSheet; }; +type Row = any[]; +type RowCol = { rows: Row[]; columns: GridColDef[]; }; + +function arrayify(rows: any[]): Row[] { + return rows.map(row => { + if(Array.isArray(row)) return row; + var length = Object.keys(row).length; + for(; length > 0; --length) if(row[length-1] != null) break; + return Array.from({length, ...row}); + }); +} + +/* this method returns `rows` and `columns` data for sheet change */ +const getRowsCols = ( data: DataSet, sheetName: string ): RowCol => ({ + rows: utils.sheet_to_json(data[sheetName], {header:1}).map((r,id) => ({...r, id})), + columns: Array.from({ + length: utils.decode_range(data[sheetName]["!ref"]||"A1").e.c + 1 + }, (_, i) => ({ field: String(i), headerName: utils.encode_col(i), editable: true })) +}); + +export default function App() { + const [rows, setRows] = useState([]); // data rows + const [columns, setColumns] = useState([]); // columns + const [workBook, setWorkBook] = useState({} as DataSet); // workbook + const [sheets, setSheets] = useState([]); // list of sheet names + const [current, setCurrent] = useState(""); // selected sheet + + /* called when sheet dropdown is changed */ + function selectSheet(name: string) { + /* update workbook cache in case the current worksheet was changed */ + workBook[current] = utils.aoa_to_sheet(arrayify(rows)); + + /* get data for desired sheet and update state */ + const { rows: new_rows, columns: new_columns } = getRowsCols(workBook, name); + setRows(new_rows); + setColumns(new_columns); + setCurrent(name); + } + + /* this method handles refreshing the state with new workbook data */ + async function handleAB(file: ArrayBuffer): Promise { + /* read file data */ + const data = read(file); + + /* update workbook state */ + setWorkBook(data.Sheets); + setSheets(data.SheetNames); + + /* select the first worksheet */ + const name = data.SheetNames[0]; + const { rows: new_rows, columns: new_columns } = getRowsCols(data.Sheets, name); + setRows(new_rows); + setColumns(new_columns); + setCurrent(name); + } + + /* called when file input element is used to select a new file */ + async function handleFile(ev: ChangeEvent): Promise { + const file = await ev.target.files?.[0]?.arrayBuffer(); + if(file) await handleAB(file); + } + + /* when page is loaded, fetch and processs worksheet */ + useEffect(() => { (async () => { + const f = await fetch("https://sheetjs.com/pres.numbers"); + await handleAB(await f.arrayBuffer()); + })(); }, []); + + /* method is called when one of the save buttons is clicked */ + function saveFile(ext: string): void { + console.log(rows); + /* update current worksheet in case changes were made */ + workBook[current] = utils.aoa_to_sheet(arrayify(rows)); + + /* construct workbook and loop through worksheets */ + const wb = utils.book_new(); + sheets.forEach((n) => { utils.book_append_sheet(wb, workBook[n], n); }); + + /* generate a file and download it */ + writeFile(wb, "SheetJSMUIDG." + ext); + } + + return ( + <> +

SheetJS × MUI Data Grid Demo

+ + {sheets.length > 0 && ( <> +

Use the dropdown to switch to a worksheet:  + +

+
Current Sheet: {current}
+
+ +
+

Click one of the buttons to create a new file with the modified data

+
{["xlsx", "xlsb", "xls"].map((ext) => ( + + ))}
+ )} + + ); +} diff --git a/docz/static/pouchdb/master.zip b/docz/static/pouchdb/master.zip new file mode 100644 index 0000000000000000000000000000000000000000..fc7b0559de5cd536d585390c2510dbe8d487a372 GIT binary patch literal 8362 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 literal 0 HcmV?d00001 diff --git a/docz/static/rdg/App.tsx b/docz/static/rdg/App.tsx new file mode 100644 index 00000000..80616d7c --- /dev/null +++ b/docz/static/rdg/App.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState, ChangeEvent } from "react"; +import DataGrid, { textEditor, Column } from "react-data-grid"; +import { read, utils, WorkSheet, writeFile } from "xlsx"; + +import './App.css'; + +type DataSet = { [index: string]: WorkSheet; }; +type Row = any[]; +type AOAColumn = Column; +type RowCol = { rows: Row[]; columns: AOAColumn[]; }; + +function arrayify(rows: any[]): Row[] { + return rows.map(row => { + if(Array.isArray(row)) return row; + var length = Object.keys(row).length; + for(; length > 0; --length) if(row[length-1] != null) break; + return Array.from({length, ...row}); + }); +} + +/* this method returns `rows` and `columns` data for sheet change */ +const getRowsCols = ( data: DataSet, sheetName: string ): RowCol => ({ + rows: utils.sheet_to_json(data[sheetName], {header:1}), + columns: Array.from({ + length: utils.decode_range(data[sheetName]["!ref"]||"A1").e.c + 1 + }, (_, i) => ({ key: String(i), name: utils.encode_col(i), editor: textEditor })) +}); + +export default function App() { + const [rows, setRows] = useState([]); // data rows + const [columns, setColumns] = useState([]); // columns + const [workBook, setWorkBook] = useState({} as DataSet); // workbook + const [sheets, setSheets] = useState([]); // list of sheet names + const [current, setCurrent] = useState(""); // selected sheet + + /* called when sheet dropdown is changed */ + function selectSheet(name: string) { + /* update workbook cache in case the current worksheet was changed */ + workBook[current] = utils.aoa_to_sheet(arrayify(rows)); + + /* get data for desired sheet and update state */ + const { rows: new_rows, columns: new_columns } = getRowsCols(workBook, name); + setRows(new_rows); + setColumns(new_columns); + setCurrent(name); + } + + /* this method handles refreshing the state with new workbook data */ + async function handleAB(file: ArrayBuffer): Promise { + /* read file data */ + const data = read(file); + + /* update workbook state */ + setWorkBook(data.Sheets); + setSheets(data.SheetNames); + + /* select the first worksheet */ + const name = data.SheetNames[0]; + const { rows: new_rows, columns: new_columns } = getRowsCols(data.Sheets, name); + setRows(new_rows); + setColumns(new_columns); + setCurrent(name); + } + + /* called when file input element is used to select a new file */ + async function handleFile(ev: ChangeEvent): Promise { + const file = await ev.target.files?.[0]?.arrayBuffer(); + if(file) await handleAB(file); + } + + /* when page is loaded, fetch and processs worksheet */ + useEffect(() => { (async () => { + const f = await fetch("https://sheetjs.com/pres.numbers"); + await handleAB(await f.arrayBuffer()); + })(); }, []); + + /* method is called when one of the save buttons is clicked */ + function saveFile(ext: string): void { + console.log(rows); + /* update current worksheet in case changes were made */ + workBook[current] = utils.aoa_to_sheet(arrayify(rows)); + + /* construct workbook and loop through worksheets */ + const wb = utils.book_new(); + sheets.forEach((n) => { utils.book_append_sheet(wb, workBook[n], n); }); + + /* generate a file and download it */ + writeFile(wb, "SheetJSRDG." + ext); + } + + return ( + <> +

SheetJS × React-Data-Grid Demo

+ + {sheets.length > 0 && ( <> +

Use the dropdown to switch to a worksheet:  + +

+
Current Sheet: {current}
+ +

Click one of the buttons to create a new file with the modified data

+
{["xlsx", "xlsb", "xls"].map((ext) => ( + + ))}
+ )} + + ); +} diff --git a/docz/static/react/rdg1.png b/docz/static/rdg/rdg1.png similarity index 100% rename from docz/static/react/rdg1.png rename to docz/static/rdg/rdg1.png