This commit is contained in:
SheetJS 2022-08-17 03:10:01 -04:00
parent 99dd5c8834
commit d4a38231dd
7 changed files with 559 additions and 1 deletions

@ -151,3 +151,218 @@ a download. By setting the `filename` and `sheetname` options in the `ui-grid`
options, the output can be controlled.
</details>
## 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.
:::
[`react-data-grid`](https://github.com/adazzle/react-data-grid) is a data grid
built for React. `react-data-grid` powers <https://sheet.js.org/>
[A complete example is included below.](#rdg-demo)
#### 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,
the columns must be objects whose `key` property is the stringified number:
```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`:
```ts
import { WorkSheet, utils } from 'xlsx';
type Row = any[];
function ws_to_rdg(rows: Row[]): WorkSheet {
return utils.aoa_to_sheet(rows);
}
```
#### RDG Demo
<details><summary><b>Complete Example</b> (click to show)</summary>
1) Create a new TypeScript CRA 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) 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-specific 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<Row>;
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<Row>(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<Row[]>([]); // data rows
const [columns, setColumns] = useState<AOAColumn[]>([]); // columns
const [workBook, setWorkBook] = useState<DataSet>({} as DataSet); // workbook
const [sheets, setSheets] = useState<string[]>([]); // list of sheet names
const [current, setCurrent] = useState<string>(""); // 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<void> {
// 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<HTMLInputElement>): Promise<void> {
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 (
<>
<h3>SheetJS × React-Data-Grid Demo</h3>
<input type="file" onChange={handleFile} />
{sheets.length > 0 && ( <>
<p>Use the dropdown to switch to a worksheet:&nbsp;
<select onChange={async (e) => selectSheet(sheets[+(e.target.value)])}>
{sheets.map((sheet, idx) => (<option key={sheet} value={idx}>{sheet}</option>))}
</select>
</p>
<div className="flex-cont"><b>Current Sheet: {current}</b></div>
<DataGrid columns={columns} rows={rows} onRowsChange={setRows} />
<p>Click one of the buttons to create a new file with the modified data</p>
<div className="flex-cont">{["xlsx", "xlsb", "xls"].map((ext) => (
<button key={ext} onClick={() => saveFile(ext)}>export [.{ext}]</button>
))}</div>
</>)}
</>
);
}
```
4) run `npm start`. When you load the dev page in the browser, it will attempt
to fetch <https://sheetjs.com/pres.numbers> and load the data.
</details>
The following screenshot was taken from the demo:
![react-data-grid screenshot](pathname:///react/rdg1.png)

@ -0,0 +1,179 @@
---
sidebar_position: 20
title: ReactJS
---
[ReactJS](https://reactjs.org/) is a JS library for building user interfaces.
This demo tries to cover common React data flow ideas and strategies. React
familiarity is assumed.
Other demos cover general React deployments, including:
- [Static Site Generation powered by NextJS](./content#nextjs)
- [iOS and Android applications powered by React Native](./mobile#react-native)
- [React Data Grid UI component](./grid#react-data-grid)
## Installation
[The "Frameworks" section](../getting-started/installation/frameworks) covers
installation with Yarn and other package managers.
The library can be imported directly from JS or JSX code with:
```js
import { read, utils, writeFile } from 'xlsx';
```
## Internal State
The various SheetJS APIs work with various data shapes. The preferred state
depends on the application.
### Array of Objects
Typically, some users will create a spreadsheet with source data that should be
loaded into the site. This sheet will have known columns. For example, our
[presidents sheet](https://sheetjs.com/pres.xlsx) has "Name" / "Index" columns:
![`pres.xlsx` data](pathname:///react/pres.png)
This naturally maps to an array of typed objects, as in the TS example below:
```ts
import { read, utils } from 'xlsx';
interface President {
Name: string;
Index: number;
}
const f = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
const wb = read(f);
const data = utils.sheet_to_json<President>(wb.Sheets[wb.SheetNames[0]]);
console.log(data);
```
`data` will be an array of objects:
```js
[
{ Name: "Bill Clinton", Index: 42 },
{ Name: "GeorgeW Bush", Index: 43 },
{ Name: "Barack Obama", Index: 44 },
{ Name: "Donald Trump", Index: 45 },
{ Name: "Joseph Biden", Index: 46 }
]
```
A component will typically map over the data. The following example generates
a TABLE with a row for each President:
```tsx title="src/SheetJSReactAoO.tsx"
import React, { useEffect, useState } from "react";
import { read, utils } from 'xlsx';
interface President { Name: string; Index: number; }
export default function SheetJSReactAoO() {
/* the component state is an array of presidents */
const [pres, setPres] = useState<President[]>([]);
/* Fetch and update the state once */
useEffect(() => { (async() => {
const f = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
// highlight-start
const wb = read(f); // parse the array buffer
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
const data = utils.sheet_to_json<President>(ws); // generate objects
setPres(data); // update state
// highlight-end
})(); }, []);
return (<table><thead><th>Name</th><th>Index</th></thead><tbody>
{ /* generate row for each president */
// highlight-start
pres.map(pres => (<tr>
<td>{pres.Name}</td>
<td>{pres.Index}</td>
</tr>))
// highlight-end
}
</tbody></table>);
}
```
### HTML
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
The `sheet_to_html` function generates HTML that is aware of merges and other
worksheet features. React `dangerouslySetInnerHTML` attribute allows code to
set the `innerHTML` attribute, effectively inserting the code into the page:
```tsx title="src/SheetJSReactHTML.tsx"
import React, { useEffect, useState } from "react";
import { read, utils } from 'xlsx';
export default function SheetJSReactHTML() {
/* the component state is an HTML string */
const [html, setHtml] = useState<string>("");
/* Fetch and update the state once */
useEffect(() => { (async() => {
const f = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
const wb = read(f); // parse the array buffer
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
// highlight-start
const data = utils.sheet_to_html(ws); // generate HTML
setHtml(data); // update state
// highlight-end
})(); }, []);
// highlight-next-line
return ( <div dangerouslySetInnerHTML={{ __html: html }} />);
}
```
### Rows and Columns
Some data grids and UI components split worksheet state in two parts: an array
of column attribute objects and an array of row objects. The former is used to
generate column headings and for indexing into the row objects.
The safest approach is to use an array of arrays for state and to generate
column objects that map to A1-style column headers.
The [React Data Grid demo](./grid#rows-and-columns-state) uses this approach
with the following column and row structure:
```js
/* rows are generated with a simple array of arrays */
const rows = utils.sheet_to_json(worksheet, { header: 1 });
/* column objects are generated based on the worksheet range */
const range = utils.decode_range(ws["!ref"]||"A1");
const columns = Array.from({ length: range.e.c + 1 }, (_, i) => ({
/* for an array of arrays, the keys are "0", "1", "2", ... */
key: String(i),
/* column labels: encode_col translates 0 -> "A", 1 -> "B", 2 -> "C", ... */
name: XLSX.utils.encode_col(i)
}));
```
![Column labels for headers](pathname:///react/cols.png)
## Legacy Deployments
[The Standalone Scripts](../getting-started/installation/standalone) play nice
with legacy deployments that do not use a bundler.
[The legacy demo](pathname:///react/index.html) shows a simple React component
transpiled in the browser using Babel standalone library.

@ -21,7 +21,7 @@ The demo projects include small runnable examples and short explainers.
- [`Angular.JS`](./legacy#angularjs)
- [`Angular 2+ and Ionic`](https://github.com/SheetJS/SheetJS/tree/master/demos/angular2/)
- [`Knockout`](./legacy#knockout)
- [`React and NextJS`](https://github.com/SheetJS/SheetJS/tree/master/demos/react/)
- [`React`](./react)
- [`VueJS`](https://github.com/SheetJS/SheetJS/tree/master/demos/vue/)
### Front-End UI Components
@ -68,3 +68,9 @@ The demo projects include small runnable examples and short explainers.
- [`vite`](./bundler#vite)
- [`webpack`](./bundler#webpack)
- [`wmr`](./bundler#wmr)
:::note
If a demo for a library or framework is not included here, please leave a note.
:::

BIN
docz/static/react/cols.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

@ -0,0 +1,158 @@
<!DOCTYPE html>
<!-- sheetjs (C) 2013-present SheetJS http://sheetjs.com -->
<!-- vim: set ts=2: -->
<html lang="en" style="height: 100%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>SheetJS React Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<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>
<style>body, #app { height: 100%; }</style>
</head>
<body>
<div class="container-fluid"><h1><a href="http://sheetjs.com">SheetJS × React Demo</a></h1><br /></div>
<div id="app" class="container-fluid"></div>
<script type="text/babel">
/* sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */
/* Notes:
- usage: `ReactDOM.render( <SheetJSApp />, document.getElementById('app') );`
- xlsx.full.min.js is loaded in the head of the HTML page
- this script should be referenced with type="text/babel"
- babel.js in-browser transpiler should be loaded before this script
*/
const { read, writeFile } = XLSX;
const { decode_range, encode_col, sheet_to_json, aoa_to_sheet, book_new, book_append_sheet } = XLSX.utils;
/* generate an array of column objects */
const make_cols = refstr => Array.from({length: decode_range(refstr).e.c + 1}, (_, i) => ({ name: encode_col(i), key: i}));
/* main component */
function SheetJSApp() {
const [data, setData] = React.useState([]);
const [cols, setCols] = React.useState([]);
const handleFile = (file) => {
const reader = new FileReader();
reader.onload = (e) => {
/* Parse data */
const ab = e.target.result;
const wb = read(ab, { type: 'array' });
/* Get first worksheet */
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* Convert array of arrays */
const data = sheet_to_json(ws, { header: 1 });
/* Update state */
setData(data);
setCols(make_cols(ws['!ref']))
};
reader.readAsArrayBuffer(file);
}
const exportFile = () => {
/* convert state to workbook */
const ws = aoa_to_sheet(data);
const wb = book_new();
book_append_sheet(wb, ws, "SheetJS");
/* generate XLSX file and send to client */
writeFile(wb, "sheetjs.xlsx")
};
return (
<DragDropFile handleFile={handleFile}>
<div className="row"><div className="col-xs-12">
<DataInput handleFile={handleFile} />
</div></div>
<div className="row"><div className="col-xs-12">
{data.length ? <button className="btn btn-success" onClick={exportFile}>Export</button> : ""}
</div></div>
<div className="row"><div className="col-xs-12">
<OutTable data={data} cols={cols} />
</div></div>
</DragDropFile>
);
}
/* -------------------------------------------------------------------------- */
/*
Simple HTML5 file drag-and-drop wrapper
usage: <DragDropFile handleFile={handleFile}>...</DragDropFile>
handleFile(file:File):void;
*/
function DragDropFile({ handleFile, children }) {
const suppress = (e) => { e.stopPropagation(); e.preventDefault(); };
const handleDrop = (e) => {
e.stopPropagation(); e.preventDefault();
const files = e.dataTransfer.files;
if (files && files[0]) handleFile(files[0]);
};
return ( <div onDrop={handleDrop} onDragEnter={suppress} onDragOver={suppress}>{children}</div> );
}
/*
Simple HTML5 file input wrapper
usage: <DataInput handleFile={callback} />
handleFile(file:File):void;
*/
function DataInput({ handleFile }) {
const handleChange = (e) => {
const files = e.target.files;
if (files && files[0]) handleFile(files[0]);
};
return (
<form className="form-inline">
<div className="form-group">
<label htmlFor="file">Drag or choose a spreadsheet file</label><br />
<input type="file" className="form-control" id="file" accept={SheetJSFT} onChange={handleChange} />
</div>
</form>
)
}
/* list of supported file types */
const SheetJSFT = [
"xlsx", "xlsb", "xlsm", "xls", "xml", "csv", "txt", "ods", "fods", "uos", "sylk", "dif", "dbf", "prn", "qpw", "123", "wb*", "wq*", "html", "htm"
].map(x => `.${x}`).join(",");
/*
Simple HTML Table
usage: <OutTable data={data} cols={cols} />
data:Array<Array<any> >;
cols:Array<{name:string, key:number|string}>;
*/
function OutTable({ data, cols }) {
return (
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>{cols.map((c) => <th key={c.key}>{c.name}</th>)}</tr>
</thead>
<tbody>
{data.map((r, i) => <tr key={i}>
{cols.map(c => <td key={c.key}>{r[c.key]}</td>)}
</tr>)}
</tbody>
</table>
</div>
);
}
/* React 18 uses ReactDOM.createRoot; < 18 should use ReactDOM.render */
const root_elt = document.getElementById('app');
if(typeof ReactDOM.createRoot !== "undefined") {
const root = ReactDOM.createRoot(root_elt);
root.render(<SheetJSApp/>);
} else {
ReactDOM.render(<SheetJSApp />, root_elt);
}
</script>
</body>
</html>

BIN
docz/static/react/pres.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docz/static/react/rdg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB