diff --git a/docz/docs/03-demos/01-frontend/01-react.md b/docz/docs/03-demos/01-frontend/01-react.md index c08ed23..3d9ee2a 100644 --- a/docz/docs/03-demos/01-frontend/01-react.md +++ b/docz/docs/03-demos/01-frontend/01-react.md @@ -16,6 +16,7 @@ Other demos cover general React deployments, including: - [iOS and Android applications powered by React Native](/docs/demos/mobile/reactnative) - [Desktop application powered by React Native Windows + macOS](/docs/demos/desktop/reactnative) - [React Data Grid UI component](/docs/demos/grid#react-data-grid) +- [Glide Data Grid UI component](/docs/demos/grid#glide-data-grid) ## Installation diff --git a/docz/docs/03-demos/05-grid.md b/docz/docs/03-demos/05-grid.md index cad4f43..cec0615 100644 --- a/docz/docs/03-demos/05-grid.md +++ b/docz/docs/03-demos/05-grid.md @@ -331,6 +331,16 @@ function ws_to_rdg(ws: WorkSheet): RowCol { 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 @@ -338,8 +348,6 @@ 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'; @@ -360,14 +368,311 @@ function rdg_to_ws(rows: Row[]): WorkSheet { } ``` -```ts -import { WorkSheet, utils } from 'xlsx'; +::: -type Row = any[]; -function rdg_to_ws(rows: Row[]): WorkSheet { - return utils.aoa_to_sheet(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 diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index 564e693..32c0a04 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -34,6 +34,7 @@ run in the web browser, demos will include interactive examples. - [`canvas-datagrid`](/docs/demos/grid#canvas-datagrid) - [`x-spreadsheet`](/docs/demos/grid#x-spreadsheet) - [`react-data-grid`](/docs/demos/grid#react-data-grid) +- [`glide-data-grid`](/docs/demos/grid#glide-data-grid) - [`vue3-table-lite`](/docs/demos/grid#vue3-table-lite) - [`angular-ui-grid`](/docs/demos/grid#angular-ui-grid) - [`material ui`](/docs/demos/grid#material-ui-table) diff --git a/docz/static/gdg/App.tsx b/docz/static/gdg/App.tsx new file mode 100644 index 0000000..9ad32ce --- /dev/null +++ b/docz/static/gdg/App.tsx @@ -0,0 +1,82 @@ +import { useState, useCallback, useEffect, useRef, ChangeEvent } from 'react'; +import { utils, read, writeFileXLSX, WorkBook } from 'xlsx'; +import { DataEditor, GridCellKind, GridCell, GridColumn, Item, DataEditorRef, EditableGridCell } from '@glideapps/glide-data-grid'; +import "@glideapps/glide-data-grid/dist/index.css"; + +// this will store the raw data objects +let data: any[] = []; +// this will store the header names +let header: string[] = []; + +function App() { + const [cols, setCols] = useState([]); // gdg column objects + const [rows, setRows] = useState(0); // number of rows + const ref = useRef(null); // gdg ref + + // read/write between gdg and the backing data store + const getContent = useCallback((cell: Item): GridCell => { + const [col, row] = cell; + return { + kind: GridCellKind.Text, + allowOverlay: true, + readonly: false, + displayData: String(data[row]?.[header[col]]??""), + data: data[row]?.[header[col]], + }; + }, []); + const onCellEdited = useCallback((cell: Item, newValue: EditableGridCell) => { + const [ col, row ] = cell; + data[row][header[col]] = newValue.data; + }, []); + + // update the data store from a workbook object + const parse_wb = (wb: WorkBook) => { + const sheet = wb.Sheets[wb.SheetNames[0]]; + data = utils.sheet_to_json(sheet); + const range = utils.decode_range(sheet["!ref"]??"A1"); range.e.r = range.s.r; + header = utils.sheet_to_json(sheet, {header: 1, range})[0]; + setCols(header.map(h => ({title: h, id: h} as GridColumn))); + setRows(data.length); + if(data.length > 0) { + let cells = data.map( + (_,R) => Array.from({length:header.length}, (_,C) => ({cell: ([C,R] as Item)})) + ).flat(); + ref.current?.updateCells(cells); + } + }; + // file input element onchange event handler + const onChange = useCallback(async (e: ChangeEvent) => { + if(!e.target?.files) return; + parse_wb(read(await e.target.files[0].arrayBuffer())); + }, []); + // when the component loads, fetch and display a sample workbook + useEffect(() => { + (async() => { + parse_wb(read(await (await fetch("https://sheetjs.com/pres.numbers")).arrayBuffer())); + })(); + }, []); + + // export data + const exportXLSX = useCallback(() => { + // 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"}); + // 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 ( <> + + +
+ +
+
+ + ) +} +export default App; diff --git a/docz/static/gdg/post.png b/docz/static/gdg/post.png new file mode 100644 index 0000000..be66ae8 Binary files /dev/null and b/docz/static/gdg/post.png differ diff --git a/docz/static/gdg/pre.png b/docz/static/gdg/pre.png new file mode 100644 index 0000000..fb399c2 Binary files /dev/null and b/docz/static/gdg/pre.png differ