2022-08-01 05:34:23 +00:00
|
|
|
---
|
2022-08-26 19:21:53 +00:00
|
|
|
title: Data Grids and Tables
|
2023-02-28 11:40:44 +00:00
|
|
|
pagination_prev: demos/frontend/index
|
|
|
|
pagination_next: demos/net/index
|
2022-08-01 05:34:23 +00:00
|
|
|
---
|
|
|
|
|
2023-04-27 09:12:19 +00:00
|
|
|
import current from '/version.js';
|
2023-05-03 03:40:40 +00:00
|
|
|
import CodeBlock from '@theme/CodeBlock';
|
2023-04-27 09:12:19 +00:00
|
|
|
|
2022-08-01 05:34:23 +00:00
|
|
|
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,
|
2022-08-25 08:22:28 +00:00
|
|
|
it is easier to process an array of arrays.
|
2022-08-01 05:34:23 +00:00
|
|
|
|
|
|
|
|
2022-08-18 08:41:34 +00:00
|
|
|
### x-spreadsheet
|
|
|
|
|
2023-02-28 11:40:44 +00:00
|
|
|
With a familiar UI, `x-spreadsheet` is an excellent choice for a modern editor.
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
[Click here for a live integration demo.](pathname:///xspreadsheet/)
|
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/xs)**
|
2022-08-18 08:41:34 +00:00
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
### Canvas Datagrid
|
2022-08-01 05:34:23 +00:00
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
After extensive testing, `canvas-datagrid` stood out as a high-performance grid
|
|
|
|
with a straightforward API.
|
2022-08-01 05:34:23 +00:00
|
|
|
|
|
|
|
[Click here for a live integration demo.](pathname:///cdg/index.html)
|
|
|
|
|
2023-03-12 06:25:57 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/cdg)**
|
2022-08-24 23:48:22 +00:00
|
|
|
|
|
|
|
### Tabulator
|
|
|
|
|
2022-10-19 21:12:12 +00:00
|
|
|
[Tabulator](https://tabulator.info/docs/5.4/download#xlsx) includes deep support
|
2022-08-25 08:22:28 +00:00
|
|
|
through a special Export button. It handles the SheetJS operations internally.
|
2022-08-24 23:48:22 +00:00
|
|
|
|
|
|
|
|
2022-08-01 05:34:23 +00:00
|
|
|
### 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.
|
|
|
|
|
2023-04-09 00:20:50 +00:00
|
|
|
The [AngularJS demo](/docs/demos/frontend/angularjs) covers more general strategies.
|
2022-08-01 05:34:23 +00:00
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
[Click here for a live integration demo.](pathname:///angularjs/ui-grid.html)
|
|
|
|
|
|
|
|
<details><summary><b>Notes</b> (click to show)</summary>
|
|
|
|
|
|
|
|
The library does not provide any way to modify the import button, so the demo
|
2022-08-25 08:22:28 +00:00
|
|
|
includes a simple directive for a File Input HTML element. It also includes a
|
2022-08-01 05:34:23 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
</details>
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
## 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
|
|
|
|
|
2023-04-19 08:50:07 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/rdg)**
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2023-02-07 09:24:49 +00:00
|
|
|
### Glide Data Grid
|
|
|
|
|
2023-04-07 08:30:20 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/gdg)**
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
### Material UI Data Grid
|
|
|
|
|
|
|
|
Material UI Data Grid and React Data Grid share many state patterns and idioms.
|
2023-04-19 08:50:07 +00:00
|
|
|
Differences from ["React Data Grid"](/docs/demos/grid/rdg) will be highlighted.
|
2022-09-03 10:02:45 +00:00
|
|
|
|
|
|
|
[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";
|
2022-08-17 07:10:01 +00:00
|
|
|
|
|
|
|
export default function App() {
|
2022-09-03 10:02:45 +00:00
|
|
|
const [rows, setRows] = useState([]);
|
|
|
|
const [columns, setColumns] = useState([]);
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
return ( <DataGrid columns={columns} rows={rows} onRowsChange={setRows} /> );
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
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[]; };
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
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) => ({
|
2022-08-17 07:10:01 +00:00
|
|
|
// highlight-start
|
2022-09-03 10:02:45 +00:00
|
|
|
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
|
2022-08-17 07:10:01 +00:00
|
|
|
// highlight-end
|
2022-09-03 10:02:45 +00:00
|
|
|
}));
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
return { rows, columns }; // these can be fed to setRows / setColumns
|
|
|
|
}
|
|
|
|
```
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
In the other direction, a worksheet can be generated with `aoa_to_sheet`:
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
:::caution
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
`x-data-grid` does not properly preserve row array objects, so the row arrays
|
|
|
|
must be re-created. The snippet defines a `arrayify` function.
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
:::
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
```ts
|
|
|
|
import { WorkSheet, utils } from 'xlsx';
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
type Row = any[];
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
// 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));
|
2022-08-17 07:10:01 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
<!-- spellchecker-disable -->
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
#### MUIDG Demo
|
|
|
|
|
|
|
|
<!-- spellchecker-enable -->
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
0) [Follow the React Data Grid demo](#rdg-demo) and generate the sample app.
|
|
|
|
|
|
|
|
1) Install dependencies:
|
|
|
|
|
2023-05-03 03:40:40 +00:00
|
|
|
<CodeBlock language="bash">{`\
|
|
|
|
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz @mui/x-data-grid @emotion/react @emotion/styled`}
|
|
|
|
</CodeBlock>
|
2022-09-03 10:02:45 +00:00
|
|
|
|
|
|
|
2) Download [`App.tsx`](pathname:///muidg/App.tsx) and replace `src/App.tsx`.
|
2022-08-17 07:10:01 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
3) run `npm start`. When you load the page in the browser, it will attempt to
|
|
|
|
fetch <https://sheetjs.com/pres.numbers> and load the data.
|
2022-08-18 08:41:34 +00:00
|
|
|
|
|
|
|
</details>
|
|
|
|
|
2022-08-25 08:22:28 +00:00
|
|
|
<!-- spellchecker-disable -->
|
|
|
|
|
2022-08-18 08:41:34 +00:00
|
|
|
### vue3-table-lite
|
|
|
|
|
2022-08-25 08:22:28 +00:00
|
|
|
<!-- spellchecker-enable -->
|
|
|
|
|
2023-04-24 08:50:42 +00:00
|
|
|
**[The exposition has been moved to a separate page.](/docs/demos/grid/vtl)**
|
2022-08-18 08:41:34 +00:00
|
|
|
|
2022-09-03 10:02:45 +00:00
|
|
|
## 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`:
|
|
|
|
|
2023-05-03 03:40:40 +00:00
|
|
|
<CodeBlock language="html">{`\
|
|
|
|
<script src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/shim.min.js"></script>
|
|
|
|
<script src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js"></script>
|
|
|
|
\n\
|
|
|
|
<!-- table with id \`xport\` -->
|
2022-09-03 10:02:45 +00:00
|
|
|
<table id="xport"><tr><td>SheetJS</td></tr></table>
|
2023-05-03 03:40:40 +00:00
|
|
|
\n\
|
2022-09-03 10:02:45 +00:00
|
|
|
<script>
|
|
|
|
/* as long as this script appears after the table, it will be visible */
|
|
|
|
var tbl = document.getElementById("xport");
|
|
|
|
const wb = XLSX.utils.table_to_book(tbl);
|
|
|
|
XLSX.writeFile(wb, "HTMLTable.xlsx");
|
2023-05-03 03:40:40 +00:00
|
|
|
</script>`}
|
|
|
|
</CodeBlock>
|
2022-09-03 10:02:45 +00:00
|
|
|
|
|
|
|
When programmatically constructing the table in the browser, retain a reference:
|
|
|
|
|
|
|
|
```js
|
2023-05-03 03:40:40 +00:00
|
|
|
/* assemble table */
|
2022-09-03 10:02:45 +00:00
|
|
|
var tbl = document.createElement("TABLE");
|
|
|
|
tbl.insertRow(0).insertCell(0).innerHTML = "SheetJS";
|
2023-05-03 03:40:40 +00:00
|
|
|
|
|
|
|
/* add to document body */
|
2022-09-03 10:02:45 +00:00
|
|
|
document.body.appendChild(tbl);
|
2023-05-03 03:40:40 +00:00
|
|
|
|
|
|
|
/* generate workbook and export */
|
2022-09-03 10:02:45 +00:00
|
|
|
const wb = XLSX.utils.table_to_book(tbl);
|
|
|
|
XLSX.writeFile(wb, "HTMLFlicker.xlsx");
|
2023-05-03 03:40:40 +00:00
|
|
|
|
|
|
|
/* remove from document body */
|
2022-09-03 10:02:45 +00:00
|
|
|
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 ( <>
|
|
|
|
<button onClick={() => {
|
|
|
|
// highlight-next-line
|
|
|
|
const wb = XLSX.utils.table_to_book(tbl.current);
|
|
|
|
XLSX.writeFile(wb, "ReactTable.xlsx");
|
|
|
|
}}>Export</button>
|
|
|
|
// highlight-next-line
|
|
|
|
<table ref={tbl}>
|
|
|
|
{/* ... TR and TD/TH elements ... */}
|
|
|
|
</table>
|
2023-02-28 11:40:44 +00:00
|
|
|
</> );
|
2022-09-03 10:02:45 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Material UI Table
|
|
|
|
|
|
|
|
The `Table` component abstracts the `<table>` 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<HTMLTableElement>(null);
|
2023-02-28 11:40:44 +00:00
|
|
|
return ( <>
|
2022-09-03 10:02:45 +00:00
|
|
|
<button onClick={() => {
|
|
|
|
const wb = utils.table_to_book(tbl.current);
|
|
|
|
writeFileXLSX(wb, "SheetJSMaterialUI.xlsx");
|
|
|
|
}}>Export</button>
|
|
|
|
<TableContainer {...}>
|
|
|
|
// highlight-next-line
|
|
|
|
<Table {...} ref={tbl}>
|
|
|
|
{/* ... material ui table machinations ... */}
|
|
|
|
</Table>
|
|
|
|
</TableContainer>
|
|
|
|
<>);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
1) Create a new TypeScript `create-react-app` app:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npx create-react-app sheetjs-mui --template typescript
|
|
|
|
cd sheetjs-mui
|
|
|
|
```
|
|
|
|
|
|
|
|
2) Install dependencies:
|
|
|
|
|
2023-05-03 03:40:40 +00:00
|
|
|
<CodeBlock language="bash">{`\
|
|
|
|
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz @mui/material`}
|
|
|
|
</CodeBlock>
|
2022-09-03 10:02:45 +00:00
|
|
|
|
|
|
|
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<HTMLTableElement>(null);
|
|
|
|
return ( <>
|
|
|
|
<button onClick={() => {
|
|
|
|
const wb = utils.table_to_book(tbl.current);
|
|
|
|
writeFileXLSX(wb, "SheetJSMaterialUI.xlsx");
|
|
|
|
}}>Export</button>
|
|
|
|
// highlight-end
|
|
|
|
<TableContainer component={Paper}>
|
|
|
|
// highlight-next-line
|
|
|
|
<Table sx={{ minWidth: 650 }} aria-label="simple table" ref={tbl}>
|
|
|
|
<TableHead>
|
|
|
|
<TableRow>
|
|
|
|
<TableCell>Dessert (100g serving)</TableCell>
|
|
|
|
<TableCell align="right">Calories</TableCell>
|
|
|
|
<TableCell align="right">Fat (g)</TableCell>
|
|
|
|
<TableCell align="right">Carbs (g)</TableCell>
|
|
|
|
<TableCell align="right">Protein (g)</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
</TableHead>
|
|
|
|
<TableBody>
|
|
|
|
{rows.map((row) => (
|
|
|
|
<TableRow
|
|
|
|
key={row.name}
|
|
|
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
|
|
|
>
|
|
|
|
<TableCell component="th" scope="row">
|
|
|
|
{row.name}
|
|
|
|
</TableCell>
|
|
|
|
<TableCell align="right">{row.calories}</TableCell>
|
|
|
|
<TableCell align="right">{row.fat}</TableCell>
|
|
|
|
<TableCell align="right">{row.carbs}</TableCell>
|
|
|
|
<TableCell align="right">{row.protein}</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
))}
|
|
|
|
</TableBody>
|
|
|
|
</Table>
|
|
|
|
</TableContainer>
|
|
|
|
// highlight-next-line
|
|
|
|
</> );
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
4) run `npm start`. Click the "Export" button and inspect the generated file.
|
|
|
|
|
|
|
|
</details>
|