diff --git a/docz/docs/03-demos/14-grid.md b/docz/docs/03-demos/14-grid.md index aa1a5439..e6d45b94 100644 --- a/docz/docs/03-demos/14-grid.md +++ b/docz/docs/03-demos/14-grid.md @@ -151,3 +151,218 @@ 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. + +::: + +[`react-data-grid`](https://github.com/adazzle/react-data-grid) is a data grid +built for React. `react-data-grid` powers + +[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 ( ); +} +``` + +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; +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 + +
Complete Example (click to show) + +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; +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) => ( + + ))}
+ )} + + ); +} +``` + +4) run `npm start`. When you load the dev 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) diff --git a/docz/docs/03-demos/21-react.md b/docz/docs/03-demos/21-react.md new file mode 100644 index 00000000..c016e1ed --- /dev/null +++ b/docz/docs/03-demos/21-react.md @@ -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(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([]); + + /* 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(ws); // generate objects + setPres(data); // update state + // highlight-end + })(); }, []); + + return ( + { /* generate row for each president */ +// highlight-start + pres.map(pres => ( + + + )) +// highlight-end + } +
NameIndex
{pres.Name}{pres.Index}
); +} +``` + +### 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(""); + + /* 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 (
); +} +``` + +### 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. \ No newline at end of file diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index fd9cf3e8..74dc5e70 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -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. + +::: diff --git a/docz/static/react/cols.png b/docz/static/react/cols.png new file mode 100644 index 00000000..e5c4204c Binary files /dev/null and b/docz/static/react/cols.png differ diff --git a/docz/static/react/index.html b/docz/static/react/index.html new file mode 100644 index 00000000..d75e8d4b --- /dev/null +++ b/docz/static/react/index.html @@ -0,0 +1,158 @@ + + + + + + + SheetJS React Demo + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/docz/static/react/pres.png b/docz/static/react/pres.png new file mode 100644 index 00000000..f2d4311a Binary files /dev/null and b/docz/static/react/pres.png differ diff --git a/docz/static/react/rdg1.png b/docz/static/react/rdg1.png new file mode 100644 index 00000000..bd4ded09 Binary files /dev/null and b/docz/static/react/rdg1.png differ