rn-desktop

This commit is contained in:
SheetJS 2023-01-04 22:57:48 -05:00
parent b8eeab685c
commit 3616b04348
13 changed files with 1773 additions and 1687 deletions

File diff suppressed because it is too large Load Diff

@ -0,0 +1,229 @@
---
title: Electron
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 1
sidebar_custom_props:
summary: Embedded NodeJS + Chromium
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main or the renderer thread.
Electron presents a `fs` module. The `require('xlsx')` call loads the CommonJS
module, so `XLSX.readFile` and `XLSX.writeFile` work in the renderer thread.
This demo was tested on 2022 November 07 with Electron 21.2.2 on `darwin-x64`.
<details><summary><b>Complete Example</b> (click to show)</summary>
This demo includes a drag-and-drop box as well as a file input box, mirroring
the [SheetJS Data Preview Live Demo](https://oss.sheetjs.com/sheetjs/)
The core data in this demo is an editable HTML table. The readers build up the
table using `sheet_to_html` (with `editable:true` option) and the writers scrape
the table using `table_to_book`.
The demo project is wired for `electron-forge` to build the standalone binary.
1) Download the demo files:
- [`package.json`](pathname:///electron/package.json) : project structure
- [`main.js`](pathname:///electron/main.js) : main process script
- [`index.html`](pathname:///electron/index.html) : window page
- [`index.js`](pathname:///electron/index.js) : script loaded in render context
:::caution
Right-click each link and select "Save Link As...". Left-clicking a link will
try to load the page in your browser. The goal is to save the file contents.
:::
2) Run `npm install` to install dependencies.
3) To verify the app works, run in the test environment:
```bash
npx -y electron .
```
The app will show and you should be able to verify reading and writing by using
the relevant buttons to open files and clicking the export button.
4) To build a standalone app, run the builder:
```bash
npm run make
```
This will generate the standalone app in the `out\sheetjs-electron-...` folder.
For a recent Intel Mac, the path will be `out/sheetjs-electron-darwin-x64/`
</details>
### Writing Files
[`XLSX.writeFile`](/docs/api/write-options) writes workbooks to the file system.
`showSaveDialog` shows a Save As dialog and returns the selected file name:
```js
/* from the renderer thread */
const electron = require('@electron/remote');
/* this function will show the save dialog and try to write the workbook */
async function exportFile(workbook) {
/* show Save As dialog */
const result = await electron.dialog.showSaveDialog({
title: 'Save file as',
filters: [{
name: "Spreadsheets",
extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
}]
});
/* write file */
// highlight-next-line
XLSX.writeFile(workbook, result.filePath);
}
```
:::note
In older versions of Electron, `showSaveDialog` returned the path directly:
```js
var dialog = require('electron').remote.dialog;
function exportFile(workbook) {
var result = dialog.showSaveDialog();
XLSX.writeFile(workbook, result);
}
```
:::
### Reading Files
Electron offers 3 different ways to read files, two of which use Web APIs.
**File Input Element**
File input elements automatically map to standard Web APIs.
For example, assuming a file input element on the page:
```html
<input type="file" name="xlfile" id="xlf" />
```
The event handler would process the event as if it were a web event:
```js
async function handleFile(e) {
const file = e.target.files[0];
const data = await file.arrayBuffer();
/* data is an ArrayBuffer */
const workbook = XLSX.read(data);
/* DO SOMETHING WITH workbook HERE */
}
document.getElementById("xlf").addEventListener("change", handleFile, false);
```
**Drag and Drop**
The [drag and drop snippet](/docs/solutions/input#example-user-submissions)
applies to DIV elements on the page.
For example, assuming a DIV on the page:
```html
<div id="drop">Drop a spreadsheet file here to see sheet data</div>
```
The event handler would process the event as if it were a web event:
```js
async function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
const file = e.dataTransfer.files[0];
const data = await file.arrayBuffer();
/* data is an ArrayBuffer */
const workbook = XLSX.read(data);
/* DO SOMETHING WITH workbook HERE */
}
document.getElementById("drop").addEventListener("drop", handleDrop, false);
```
**Electron API**
[`XLSX.readFile`](/docs/api/parse-options) reads workbooks from the file system.
`showOpenDialog` shows a Save As dialog and returns the selected file name.
Unlike the Web APIs, the `showOpenDialog` flow can be initiated by app code:
```js
/* from the renderer thread */
const electron = require('@electron/remote');
/* this function will show the open dialog and try to parse the workbook */
async function importFile() {
/* show Save As dialog */
const result = await electron.dialog.showOpenDialog({
title: 'Select a file',
filters: [{
name: "Spreadsheets",
extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
}]
});
/* result.filePaths is an array of selected files */
if(result.filePaths.length == 0) throw new Error("No file was selected!");
// highlight-next-line
return XLSX.readFile(result.filePaths[0]);
}
```
:::note
In older versions of Electron, `showOpenDialog` returned the path directly:
```js
var dialog = require('electron').remote.dialog;
function importFile(workbook) {
var result = dialog.showOpenDialog({ properties: ['openFile'] });
return XLSX.readFile(result[0]);
}
```
:::
### Electron Breaking Changes
The first version of this demo used Electron 1.7.5. The current demo includes
the required changes for Electron 19.2.2.
There are no Electron-specific workarounds in the library, but Electron broke
backwards compatibility multiple times. A summary of changes is noted below.
:::caution
Electron 6.x changed the `dialog` API. Methods like `showSaveDialog` originally
returned an array of strings, but now returns a `Promise`. This change was not
documented.
Electron 9.0.0 and later require the preference `nodeIntegration: true` in order
to `require('xlsx')` in the renderer process.
Electron 12.0.0 and later also require `worldSafeExecuteJavascript: true` and
`contextIsolation: true`.
Electron 14+ must use `@electron/remote` instead of `remote`. An `initialize`
call is required to enable Developer Tools in the window.
:::

@ -0,0 +1,129 @@
---
title: NW.js
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 2
sidebar_custom_props:
summary: Embedded Chromium + NodeJS
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
The [Standalone scripts](/docs/getting-started/installation/standalone) can be
referenced in a `SCRIPT` tag from the entry point HTML page.
This demo was tested against NW.js 0.66.0.
<details><summary><b>Complete Example</b> (click to show)</summary>
1) Create a `package.json` file that specifies the entry point:
```json title="package.json"
{
"name": "sheetjs-nwjs",
"author": "sheetjs",
"version": "0.0.0",
"main": "index.html",
"dependencies": {
"nw": "~0.66.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz"
}
}
```
2) Download [`index.html`](pathname:///nwjs/index.html) into the same folder.
:::caution
Right-click the link and select "Save Link As...". Left-clicking the link will
try to load the page in your browser. The goal is to save the file contents.
:::
3) Run `npm install` to install dependencies
4) To verify the app works, run in the test environment:
```
npx nw .
```
The app will show and you should be able to verify reading and writing by using
the file input element to select a spreadsheet and clicking the export button.
5) To build a standalone app, run the builder:
```
npx -p nw-builder nwbuild --mode=build .
```
This will generate the standalone app in the `build\sheetjs-nwjs\` folder.
</details>
### Reading data
The standard HTML5 `FileReader` techniques from the browser apply to NW.js!
NW.js handles the OS minutiae for dragging files into app windows. The
[drag and drop snippet](/docs/solutions/input#example-user-submissions) apply
to DIV elements on the page.
Similarly, file input elements automatically map to standard Web APIs.
For example, assuming a file input element on the page:
```html
<input type="file" name="xlfile" id="xlf" />
```
The event handler would process the event as if it were a web event:
```js
async function handleFile(e) {
const file = e.target.files[0];
const data = await file.arrayBuffer();
/* data is an ArrayBuffer */
const workbook = XLSX.read(data);
/* DO SOMETHING WITH workbook HERE */
}
document.getElementById("xlf").addEventListener("change", handleFile, false);
```
### Writing data
File input elements with the attribute `nwsaveas` show UI for saving a file. The
standard trick is to generate a hidden file input DOM element and "click" it.
Since NW.js does not present a `writeFileSync` in the `fs` package, a manual
step is required:
```js
/* pre-build the hidden nwsaveas input element */
var input = document.createElement('input');
input.style.display = 'none';
input.setAttribute('nwsaveas', 'SheetJSNWDemo.xlsx');
input.setAttribute('type', 'file');
document.body.appendChild(input);
/* show a message if the save is canceled */
input.addEventListener('cancel',function(){ alert("Save was canceled!"); });
/* write to a file on the 'change' event */
input.addEventListener('change',function(e){
/* the `value` is the path that the program will write */
var filename = this.value;
/* use XLSX.write with type "buffer" to generate a buffer" */
/* highlight-next-line */
var wbout = XLSX.write(workbook, {type:'buffer', bookType:"xlsx"});
/* highlight-next-line */
fs.writeFile(filename, wbout, function(err) {
if(!err) return alert("Saved to " + filename);
alert("Error: " + (err.message || err));
});
});
input.click();
```

@ -0,0 +1,221 @@
---
title: Wails
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 3
sidebar_custom_props:
summary: Webview + Go Backend
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from JavaScript code.
This demo was tested against Wails `v2.0.0-beta.44.2` on 2022 August 31 using
the Svelte TypeScript starter.
:::caution
Wails currently does not provide the equivalent of NodeJS `fs` module.
The HTML File Input Element does not show a file picker. This is a known bug.
All raw file operations must be performed in Go code.
:::
The "Complete Example" creates an app that looks like the screenshot:
![SheetJS Wails MacOS screenshot](pathname:///wails/macos.png)
<details><summary><b>Complete Example</b> (click to show)</summary>
0) [Read Wails "Getting Started" guide and install dependencies.](https://wails.io/docs/gettingstarted/installation)
1) Create a new Wails app:
```bash
wails init -n sheetjs-wails -t svelte-ts
```
2) Enter the directory:
```bash
cd sheetjs-wails
```
3) Install front-end dependencies:
```bash
cd frontend
curl -L -o src/assets/logo.png https://sheetjs.com/sketch1024.png
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
cd ..
```
4) Download source files:
- Download [`app.go`](pathname:///wails/app.go) and replace `app.go`
- Download [`App.svelte`](pathname:///wails/App.svelte) and replace
`frontend/src/App.svelte`
5) Build the app with
```bash
wails build
```
At the end, it will print the path to the generated program. Run the program!
</details>
All operations must be run from Go code. This example passes Base64 strings.
### Reading Files
The file picker and reading operations can be combined in one Go function.
#### Go
```go
import (
"context"
// highlight-start
"encoding/base64"
"io/ioutil"
"github.com/wailsapp/wails/v2/pkg/runtime"
// highlight-end
)
type App struct {
ctx context.Context
}
// ReadFile shows an open file dialog and returns the data as Base64 string
func (a *App) ReadFile() string {
// highlight-next-line
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select File",
Filters: []runtime.FileFilter{
{ DisplayName: "Excel Workbooks (*.xlsx)", Pattern: "*.xlsx", },
// ... more filters for more file types
},
})
if err != nil { return "" } // The demo app shows an error message
// highlight-next-line
data, err := ioutil.ReadFile(selection)
if err != nil { return "" } // The demo app shows an error message
// highlight-next-line
return base64.StdEncoding.EncodeToString(data)
}
```
#### JS
Wails will automatically create `window.go.main.App.ReadFile` for use in JS:
```js title="frontend/src/App.svelte"
import { read, utils } from 'xlsx';
async function importFile(evt) {
// highlight-start
const b64 = window['go']['main']['App']['ReadFile']();
const wb = read(b64, { type: "base64" });
// highlight-end
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
html = utils.sheet_to_html(ws); // generate HTML and update state
}
```
### Writing Files
There is a multi-part dance since the library needs the file extension.
1) Show the save file picker in Go, pass back to JS
2) Generate the file data in JS, pass the data back to Go
3) Write to file in Go
##### Go
Two Go functions will be exposed.
- `SaveFile` will show the file picker and return the path:
```go
import (
"context"
// highlight-start
"github.com/wailsapp/wails/v2/pkg/runtime"
// highlight-end
)
type App struct {
ctx context.Context
}
func (a *App) SaveFile() string {
// highlight-next-line
selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "Select File",
DefaultFilename: "SheetJSWails.xlsx",
Filters: []runtime.FileFilter{
{ DisplayName: "Excel Workbooks (*.xlsx)", Pattern: "*.xlsx", },
// ... more filters for more file types
},
})
if err != nil { return "" } // The demo app shows an error message
return selection
}
```
- `WriteFile` performs the file write given a Base64 string and file path:
```go
import (
"context"
// highlight-start
"encoding/base64"
"io/ioutil"
// highlight-end
)
type App struct {
ctx context.Context
}
func (a *App) WriteFile(b64 string, path string) {
// highlight-start
buf, _ := base64.StdEncoding.DecodeString(b64);
_ = ioutil.WriteFile(path, buf, 0644);
// highlight-end
}
```
#### JS
Wails will automatically create bindings for use in JS:
```js
import { utils, write } from 'xlsx';
async function exportFile(wb) {
/* generate workbook */
const elt = tbl.getElementsByTagName("TABLE")[0];
const wb = utils.table_to_book(elt);
/* show save picker and get path */
const path = await window['go']['main']['App']['SaveFile']();
/* generate base64 string based on the path */
const b64 = write(wb, { bookType: path.slice(path.lastIndexOf(".")+1), type: "base64" });
/* write to file */
await window['go']['main']['App']['WriteFile'](b64, path);
// The demo shows a success message at this point
}
```

@ -0,0 +1,213 @@
---
title: Tauri
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 4
sidebar_custom_props:
summary: Webview + Rust Backend
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from JavaScript code.
This demo was tested against Tauri 1.0.5 on 2022 August 13.
:::note
Tauri currently does not provide the equivalent of NodeJS `fs` module. The raw
`@tauri-apps/api` methods used in the examples are not expected to change.
:::
`http` and `dialog` must be explicitly allowed in `tauri.conf.json`:
```json title="tauri.conf.json"
"allowlist": {
"all": true,
"http": {
"all": true,
"request": true,
"scope": ["https://**"]
},
"dialog": {
"all": true
}
```
The "Complete Example" creates an app that looks like the screenshot:
![SheetJS Tauri MacOS screenshot](pathname:///tauri/macos.png)
<details><summary><b>Complete Example</b> (click to show)</summary>
0) [Read Tauri "Getting Started" guide and install dependencies.](https://tauri.app/v1/guides/getting-started/prerequisites)
1) Create a new Tauri app:
```bash
npm create tauri-app
```
When prompted:
- App Name: `SheetJSTauri`
- Window Title: `SheetJS + Tauri`
- UI recipe: `create-vite`
- Add "@tauri-apps/api": `Y`
- ViteJS template: `vue-ts`
2) Enter the directory:
```bash
cd SheetJSTauri
```
Open `package.json` with a text editor and add the highlighted lines:
```json title="package.json"
{
"name": "SheetJSTauri",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
// highlight-next-line
"@tauri-apps/api": "^1.0.2",
"vue": "^3.2.37",
// highlight-next-line
"xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz"
},
"devDependencies": {
// highlight-next-line
"@tauri-apps/cli": "^1.0.5",
"@vitejs/plugin-vue": "^3.0.3",
"typescript": "^4.6.4",
"vite": "^3.0.7",
"vue-tsc": "^0.39.5"
}
}
```
3) Install dependencies:
```bash
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
4) Enable operations by adding the highlighted lines to `tauri.conf.json`:
```json title="src-tauri/tauri.conf.json"
"tauri": {
"allowlist": {
// highlight-start
"http": {
"all": true,
"request": true,
"scope": ["https://**"]
},
"dialog": {
"all": true
},
// highlight-end
"all": true
}
```
In the same file, look for the `"identifier"` key and replace the value with `com.sheetjs.tauri`:
```json title="src-tauri/tauri.conf.json"
"icons/icon.ico"
],
// highlight-next-line
"identifier": "com.sheetjs.tauri",
"longDescription": "",
```
5) Download [`App.vue`](pathname:///tauri/App.vue) and replace `src/App.vue`
with the downloaded script.
6) Build the app with
```bash
npm run tauri build
```
At the end, it will print the path to the generated program. Run the program!
</details>
### Reading Files
There are two steps to reading files: obtaining a path and reading binary data:
```js
import { read } from 'xlsx';
import { open } from '@tauri-apps/api/dialog';
import { readBinaryFile } from '@tauri-apps/api/fs';
const filters = [
{name: "Excel Binary Workbook", extensions: ["xlsb"]},
{name: "Excel Workbook", extensions: ["xlsx"]},
{name: "Excel 97-2004 Workbook", extensions: ["xls"]},
// ... other desired formats ...
];
async function openFile() {
/* show open file dialog */
const selected = await open({
title: "Open Spreadsheet",
multiple: false,
directory: false,
filters
});
/* read data into a Uint8Array */
const d = await readBinaryFile(selected);
/* parse with SheetJS */
const wb = read(d);
return wb;
}
```
### Writing Files
There are two steps to writing files: obtaining a path and writing binary data:
```js
import { write } from 'xlsx';
import { save } from '@tauri-apps/api/dialog';
import { writeBinaryFile } from '@tauri-apps/api/fs';
const filters = [
{name: "Excel Binary Workbook", extensions: ["xlsb"]},
{name: "Excel Workbook", extensions: ["xlsx"]},
{name: "Excel 97-2004 Workbook", extensions: ["xls"]},
// ... other desired formats ...
];
async function saveFile(wb) {
/* show save file dialog */
const selected = await save({
title: "Save to Spreadsheet",
filters
});
/* Generate workbook */
const bookType = selected.slice(selected.lastIndexOf(".") + 1);
const d = write(wb, {type: "buffer", bookType});
/* save data to file */
await writeBinaryFile(selected, d);
}
```

@ -0,0 +1,246 @@
---
title: NeutralinoJS
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 5
sidebar_custom_props:
summary: Webview + Lightweight Extensions
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
The [Standalone build](/docs/getting-started/installation/standalone) can be added
to the entry `index.html`
This demo was tested against "binaries" `4.7.0` and "client" `3.6.0`
:::note
NeutralinoJS currently does not provide the equivalent of NodeJS `fs` module.
The raw `Neutralino.filesystem` and `Neutralino.os` methods are used.
:::
The `os` and `filesystem` modules must be enabled in `neutralino.conf.json`.
The starter already enables `os` so typically one line must be added:
```json title="neutralino.config.json"
"nativeAllowList": [
"app.*",
"os.*",
// highlight-next-line
"filesystem.*",
"debug.log"
],
```
The "Complete Example" creates an app that looks like the screenshot:
![SheetJS NeutralinoJS MacOS screenshot](pathname:///neu/macos.png)
:::caution
At the time of writing, `filters` did not work as expected on MacOS. They have
been omitted in the example and commented in the code snippets
:::
<details><summary><b>Complete Example</b> (click to show)</summary>
The app core state will be the HTML table. Reading files will add the table to
the window. Writing files will parse the table into a spreadsheet.
1) Create a new NeutralinoJS app:
```bash
npx @neutralinojs/neu create sheetjs-neu
cd sheetjs-neu
```
2) Download the standalone script and place in `resources/js/main.js`:
```bash
curl -L -o resources/js/xlsx.full.min.js https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js
```
3) Add the highlighted lines to `neutralino.conf.json` in `nativeAllowList`:
```json title="neutralino.config.json"
"nativeAllowList": [
"app.*",
// highlight-start
"os.*",
"filesystem.*",
// highlight-end
"debug.log"
],
```
4) Set up skeleton app and print version info:
- Edit `resources/index.html` and replace the `<body>` with the code below:
```html title="resources/index.html"
<body>
<div id="neutralinoapp">
<h1>SheetJS × NeutralinoJS</h1>
<button onclick="importData()">Import Data</button>
<button onclick="exportData()">Export Data</button>
<div id="info"></div>
</div>
<script src="js/neutralino.js"></script>
<!-- Load the browser build and make XLSX available to main.js -->
<script src="js/xlsx.full.min.js"></script>
<script src="js/main.js"></script>
</body>
```
- Append the following code to `resources/styles.css` to center the table:
```css title="resources/styles.css"
#info {
width:100%;
text-align: unset;
}
table {
margin: 0 auto;
}
```
- Print the version number in the `showInfo` method of `resources/js/main.js`:
```js title="resources/js/main.js"
${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS}
<br/><br/>
<span>server: v${NL_VERSION} . client: v${NL_CVERSION}</span>
// highlight-start
<br/><br/>
<span>SheetJS version ${XLSX.version}</span>
// highlight-end
`;
```
5) Run the app:
```bash
npx @neutralinojs/neu run
```
You should see `SheetJS Version ` followed by the library version number.
6) Add the following code to the bottom of `resources/js/main.js`:
```js
(async() => {
const ab = await (await fetch("https://sheetjs.com/pres.numbers")).arrayBuffer();
const wb = XLSX.read(ab);
const ws = wb.Sheets[wb.SheetNames[0]];
document.getElementById('info').innerHTML = XLSX.utils.sheet_to_html(ws);
})();
```
Save the source file, close the app and re-run the command from step 5.
When the app loads, a table should show in the main screen.
7) Add `importFile` and `exportFile` to the bottom of `resources/js/main.js`:
```js
async function importData() {
/* show open dialog */
const [filename] = await Neutralino.os.showOpenDialog('Open a spreadsheet');
/* read data */
const ab = await Neutralino.filesystem.readBinaryFile(filename);
const wb = XLSX.read(ab);
/* make table */
const ws = wb.Sheets[wb.SheetNames[0]];
document.getElementById('info').innerHTML = XLSX.utils.sheet_to_html(ws);
}
async function exportData() {
/* show save dialog */
const filename = await Neutralino.os.showSaveDialog('Save to file');
/* make workbook */
const tbl = document.getElementById('info').querySelector("table");
const wb = XLSX.utils.table_to_book(tbl);
/* make file */
const bookType = filename.slice(filename.lastIndexOf(".") + 1);
const data = XLSX.write(wb, { bookType, type: "buffer" });
await Neutralino.filesystem.writeBinaryFile(filename, data);
}
```
Save the source file, close the app and re-run the command from step 5.
When the app loads, click the "Import File" button and select a spreadsheet to
see the contents. Click "Export File" and enter `SheetJSNeu.xlsx` to write.
8) Build production apps:
```bash
npx @neutralinojs/neu run
```
Platform-specific programs will be created in the `dist` folder.
</details>
### Reading Files
There are two steps to reading files: obtaining a path and reading binary data:
```js
const filters = [
{name: "Excel Binary Workbook", extensions: ["xlsb"]},
{name: "Excel Workbook", extensions: ["xlsx"]},
]
async function openFile() {
/* show open file dialog */
const [filename] = await Neutralino.os.showOpenDialog(
'Open a spreadsheet',
{ /* filters, */ multiSelections: false }
);
/* read data into an ArrayBuffer */
const ab = await Neutralino.filesystem.readBinaryFile(filename);
/* parse with SheetJS */
const wb = XLSX.read(ab);
return wb;
}
```
This method can be called from a button click or other event.
### Writing Files
There are two steps to writing files: obtaining a path and writing binary data:
```js
const filters = [
{name: "Excel Binary Workbook", extensions: ["xlsb"]},
{name: "Excel Workbook", extensions: ["xlsx"]},
]
async function saveFile(wb) {
/* show save file dialog */
const filename = await Neutralino.os.showSaveDialog(
'Save to file',
{ /* filters */ }
);
/* Generate workbook */
const bookType = filename.slice(filename.lastIndexOf(".") + 1);
const data = XLSX.write(wb, { bookType, type: "buffer" });
/* save data to file */
await Neutralino.filesystem.writeBinaryFile(filename, data);
}
```

@ -0,0 +1,683 @@
---
title: React Native for Desktop
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 6
sidebar_custom_props:
summary: Native Components with React
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::note
This section covers React Native for desktop applications. For iOS and Android
applications, [check the mobile demo](/docs/demos/mobile)
:::
React Native for Windows + macOS is a backend for React Native that supports
native apps. The Windows backend builds apps for use on Windows 10 / 11, Xbox,
and other supported platforms. The macOS backend supports macOS 10.14 SDK
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main app script. File operations must be written in native code.
The "Complete Example" creates an app that looks like the screenshots below:
<table><thead><tr>
<th><a href="#windows-demo">Windows</a></th>
<th><a href="#macos-demo">macOS</a></th>
</tr></thead><tbody><tr><td>
![Windows screenshot](pathname:///reactnative/rnw.png)
</td><td>
![macOS screenshot](pathname:///reactnative/rnm.png)
</td></tr></tbody></table>
## Native Modules
:::caution
As with the mobile versions of React Native, file operations are not provided
by the base SDK. The examples include native code for both Windows and macOS.
The Windows demo assumes some familiarity with C++ / C# and the macOS demo
assumes some familiarity with Objective-C.
:::
React Native for Windows + macOS use [Turbo Modules](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules)
for effortless integration with native libraries and code.
The demos define a native module named `DocumentPicker`.
### Reading Files
The native modules in the demos define a `PickAndRead` function that will show
the file picker, read the file contents, and return a Base64 string.
Only the main UI thread can show file pickers. This is similar to Web Worker
DOM access limitations in the Web platform.
_Integration_
This module can be referenced from the Turbo Module Registry:
```js
import { read } from 'xlsx';
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
const DocumentPicker = getEnforcing('DocumentPicker');
/* ... in some event handler ... */
async() => {
const b64 = await DocumentPicker.PickAndRead();
const wb = read(b64);
// DO SOMETHING WITH `wb` HERE
}
```
_Native Module_
<Tabs groupId="os">
<TabItem value="win" label="Windows">
React Native Windows supports C++ and C# projects.
<Tabs groupId="rnwlang">
<TabItem value="cs" label="C#">
```csharp
[ReactMethod("PickAndRead")]
public async void PickAndRead(IReactPromise<string> result) {
/* perform file picker action in the UI thread */
// highlight-next-line
context.Handle.UIDispatcher.Post(async() => { try {
/* create file picker */
var picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
picker.FileTypeFilter.Add(".xlsx");
picker.FileTypeFilter.Add(".xls");
/* show file picker */
// highlight-next-line
var file = await picker.PickSingleFileAsync();
if(file == null) throw new Exception("File not found");
/* read data and return base64 string */
var buf = await FileIO.ReadBufferAsync(file);
// highlight-next-line
result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
} catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
}
```
</TabItem>
<TabItem value="cpp" label="C++">
```cpp
REACT_METHOD(PickAndRead);
void PickAndRead(ReactPromise<winrt::hstring> promise) noexcept {
auto prom = promise;
/* perform file picker action in the UI thread */
// highlight-next-line
context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget {
auto p = prom; // promise -> prom -> p dance avoids promise destruction
/* create file picker */
FileOpenPicker picker;
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
picker.FileTypeFilter().Append(L".xlsx");
picker.FileTypeFilter().Append(L".xls");
/* show file picker */
// highlight-next-line
StorageFile file = co_await picker.PickSingleFileAsync();
if(file == nullptr) { p.Reject("File not Found"); co_return; }
/* read data and return base64 string */
auto buf = co_await FileIO::ReadBufferAsync(file);
// highlight-next-line
p.Resolve(CryptographicBuffer::EncodeToBase64String(buf));
co_return;
});
}
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="mac" label="macOS">
React Native macOS supports Objective-C modules
```objc
/* the resolve/reject is projected on the JS side as a Promise */
RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
/* perform file picker action in the UI thread */
// highlight-next-line
RCTExecuteOnMainQueue(^{
/* create file picker */
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setCanChooseDirectories:NO];
[panel setAllowsMultipleSelection:NO];
[panel setMessage:@"Select a spreadsheet to read"];
/* show file picker */
// highlight-next-line
[panel beginWithCompletionHandler:^(NSInteger result){
if (result == NSModalResponseOK) {
/* read data and return base64 string */
NSURL *selected = [[panel URLs] objectAtIndex:0];
NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil];
if(hFile) {
NSData *data = [hFile readDataToEndOfFile];
// highlight-next-line
resolve([data base64EncodedStringWithOptions:0]);
} else reject(@"read_failure", @"Could not read selected file!", nil);
} else reject(@"select_failure", @"No file selected!", nil);
}];
});
}
```
</TabItem>
</Tabs>
## Windows Demo
This demo was tested against `v0.70.10` on 2023 January 04 in Windows 10.
:::warning
There is no simple standalone executable file at the end of the process.
[The official documentation describes distribution strategies](https://microsoft.github.io/react-native-windows/docs/native-code#distribution)
:::
:::note
React Native Windows supports writing native code in C++ or C#. This demo has
been tested against both application types.
:::
0) Follow the ["Getting Started" guide](https://microsoft.github.io/react-native-windows/docs/getting-started)
:::caution
NodeJS `v16` is required. There are OS-specific tools for downgrading:
- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows
- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc.
:::
1) Create a new project using React Native `0.70`:
```powershell
npx react-native init SheetJSWin --template react-native@^0.70.0
cd .\SheetJSWin\
```
Create the Windows part of the application:
<Tabs groupId="rnwlang">
<TabItem value="cs" label="C#">
```powershell
npx react-native-windows-init --no-telemetry --overwrite --language=cs
```
</TabItem>
<TabItem value="cpp" label="C++">
```powershell
npx react-native-windows-init --no-telemetry --overwrite
```
</TabItem>
</Tabs>
Install library:
```powershell
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
To ensure that the app works, launch the app:
```powershell
npx react-native run-windows --no-telemetry
```
<Tabs groupId="rnwlang">
<TabItem value="cs" label="C#">
2) Create the file `windows\SheetJSWin\DocumentPicker.cs` with the following:
```csharp title="windows\SheetJSWin\DocumentPicker.cs"
using System;
using System.Threading.Tasks;
using Windows.Security.Cryptography;
using Windows.Storage;
using Windows.Storage.Pickers;
using Microsoft.ReactNative.Managed;
namespace SheetJSWin {
[ReactModule]
class DocumentPicker {
private ReactContext context;
[ReactInitializer]
public void Initialize(ReactContext reactContext) { context = reactContext; }
[ReactMethod("PickAndRead")]
public async void PickAndRead(IReactPromise<string> result) {
context.Handle.UIDispatcher.Post(async() => { try {
var picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
picker.FileTypeFilter.Add(".xlsx");
picker.FileTypeFilter.Add(".xls");
var file = await picker.PickSingleFileAsync();
if(file == null) throw new Exception("File not found");
var buf = await FileIO.ReadBufferAsync(file);
result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
} catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
}
}
}
```
3) Add the highlighted line to `windows\SheetJSWin\SheetJSWin.csproj`. Look for
the `ItemGroup` that contains `ReactPackageProvider.cs`:
```xml title="windows\SheetJSWin\SheetJSWin.csproj"
<!-- highlight-next-line -->
<Compile Include="DocumentPicker.cs" />
<Compile Include="ReactPackageProvider.cs" />
</ItemGroup>
```
</TabItem>
<TabItem value="cpp" label="C++">
2) Create the file `windows\SheetJSWin\DocumentPicker.h` with the following:
```cpp title="windows\SheetJSWin\DocumentPicker.h"
#pragma once
#include <winrt/Windows.Storage.Pickers.h>
#include <winrt/Windows.Security.Cryptography.h>
#include "NativeModules.h"
using namespace winrt::Microsoft::ReactNative;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Pickers;
using namespace winrt::Windows::Security::Cryptography;
namespace SheetJSWin {
REACT_MODULE(DocumentPicker);
struct DocumentPicker {
REACT_INIT(Initialize);
void Initialize(const ReactContext& reactContext) noexcept {
context = reactContext;
}
REACT_METHOD(PickAndRead);
void PickAndRead(ReactPromise<winrt::hstring> promise) noexcept {
auto prom = promise;
context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget {
auto p = prom;
FileOpenPicker picker;
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
picker.FileTypeFilter().Append(L".xlsx");
picker.FileTypeFilter().Append(L".xls");
StorageFile file = co_await picker.PickSingleFileAsync();
if(file == nullptr) { p.Reject("File not Found"); co_return; }
auto buf = co_await FileIO::ReadBufferAsync(file);
p.Resolve(CryptographicBuffer::EncodeToBase64String(buf));
co_return;
});
}
private:
ReactContext context{nullptr};
};
}
```
3) Add the highlighted line to `windows\SheetJSWin\ReactPackageProvider.cpp`:
```cpp title="windows\SheetJSWin\ReactPackageProvider.cpp"
#include "ReactPackageProvider.h"
// highlight-next-line
#include "DocumentPicker.h"
#include "NativeModules.h"
```
</TabItem>
</Tabs>
Now the native module will be added to the app.
4) Remove `App.js` and save the following to `App.tsx`:
```tsx title="App.tsx"
import React, { useState, type Node } from 'react';
import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
import { read, utils, version } from 'xlsx';
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
const DocumentPicker = getEnforcing('DocumentPicker');
const App: () => Node = () => {
const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]);
return (
<SafeAreaView style={styles.outer}>
<Text style={styles.title}>SheetJS × React Native Windows {version}</Text>
<TouchableHighlight onPress={async() => {
try {
const b64 = await DocumentPicker.PickAndRead();
const wb = read(b64);
setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } ));
} catch(err) { alert(`Error: ${err.message}`); }
}}><Text style={styles.button}>Click here to Open File!</Text></TouchableHighlight>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={styles.table}>{aoa.map((row,R) => (
<View style={styles.row} key={R}>{row.map((cell,C) => (
<View style={styles.cell} key={C}><Text>{cell}</Text></View>
))}</View>
))}</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
cell: { flex: 4 },
row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', },
table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', },
outer: { marginTop: 32, paddingHorizontal: 24, },
title: { fontSize: 24, fontWeight: '600', },
button: { marginTop: 8, fontSize: 18, fontWeight: '400', },
});
export default App;
```
5) Test the app again:
```powershell
npx react-native run-windows --no-telemetry
```
Download <https://sheetjs.com/pres.xlsx>, then click on "open file". Use the
file picker to select the `pres.xlsx` file and the app will show the data.
## macOS Demo
This demo was tested against `v0.64.30` on 2023 January 04 in MacOS 12.4
0) Follow the [React Native](https://reactnative.dev/docs/environment-setup)
guide for React Native CLI on MacOS.
:::caution
NodeJS `v16` is required. There are OS-specific tools for downgrading:
- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows
- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc.
:::
1) Create a new project using React Native `0.64`:
```bash
npx react-native init SheetJSmacOS --template react-native@^0.64.0
cd SheetJSmacOS
```
Create the MacOS part of the application:
```bash
npx react-native-macos-init --no-telemetry
```
Install Library:
```
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
To ensure that the app works, launch the app:
```powershell
npx react-native run-macos
```
Close the running app from the dock and close the Metro terminal window.
2) Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.h`:
```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.h"
#import <React/RCTBridgeModule.h>
@interface RCTDocumentPicker : NSObject <RCTBridgeModule>
@end
```
Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.m`:
```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.m"
#import <Foundation/Foundation.h>
#import <React/RCTUtils.h>
#import "RCTDocumentPicker.h"
@implementation RCTDocumentPicker
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
RCTExecuteOnMainQueue(^{
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setCanChooseDirectories:NO];
[panel setAllowsMultipleSelection:NO];
[panel setMessage:@"Select a spreadsheet to read"];
[panel beginWithCompletionHandler:^(NSInteger result){
if (result == NSModalResponseOK) {
NSURL *selected = [[panel URLs] objectAtIndex:0];
NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil];
if(hFile) {
NSData *data = [hFile readDataToEndOfFile];
resolve([data base64EncodedStringWithOptions:0]);
} else reject(@"read_failure", @"Could not read selected file!", nil);
} else reject(@"select_failure", @"No file selected!", nil);
}];
});
}
@end
```
3) Edit the project file `macos/SheetJSmacOS.xcodeproj/project.pbxproj`.
There are four places where lines must be added:
A) Immediately after `/* Begin PBXBuildFile section */`
```plist
/* Begin PBXBuildFile section */
// highlight-next-line
4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */; };
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
```
B) Immediately after `/* Begin PBXFileReference section */`
```plist
/* Begin PBXFileReference section */
// highlight-start
4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTDocumentPicker.h; path = "SheetJSMacOS-macOS/RCTDocumentPicker.h"; sourceTree = "<group>"; };
4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTDocumentPicker.m; path = "SheetJSMacOS-macOS/RCTDocumentPicker.m"; sourceTree = "<group>"; };
// highlight-end
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
```
C) The goal is to add a reference to the `PBXSourcesBuildPhase` block for the
`macOS` target. To determine this, look in the `PBXNativeTarget section` for
a block with the comment `SheetJSmacOS-macOS`:
```plist
/* Begin PBXNativeTarget section */
...
productType = "com.apple.product-type.application";
};
// highlight-next-line
514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = {
isa = PBXNativeTarget;
...
/* End PBXNativeTarget section */
```
Within the block, look for `buildPhases` and find the hex string for `Sources`:
```plist
buildPhases = (
1A938104A937498D81B3BD3B /* [CP] Check Pods Manifest.lock */,
381D8A6F24576A6C00465D17 /* Start Packager */,
// highlight-next-line
514201452437B4B30078DB4F /* Sources */,
514201462437B4B30078DB4F /* Frameworks */,
514201472437B4B30078DB4F /* Resources */,
381D8A6E24576A4E00465D17 /* Bundle React Native code and images */,
3689826CA944E2EF44FCBC17 /* [CP] Copy Pods Resources */,
);
```
Search for that hex string (`514201452437B4B30078DB4F` in our example) in the
file and it should show up in a `PBXSourcesBuildPhase` section. Within `files`,
add the highlighted line:
```plist
514201452437B4B30078DB4F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
// highlight-next-line
4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */,
514201502437B4B30078DB4F /* ViewController.m in Sources */,
514201582437B4B40078DB4F /* main.m in Sources */,
5142014D2437B4B30078DB4F /* AppDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
```
D) The goal is to add file references to the "main group". Search for
`/* Begin PBXProject section */` and there should be one Project object.
Within the project object, look for `mainGroup`:
```plist
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
...
Base,
);
// highlight-next-line
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
...
/* End PBXProject section */
```
Search for that hex string (`83CBB9F61A601CBA00E9B192` in our example) in the
file and it should show up in a `PBXGroup` section. Within `children`, add the
highlighted lines:
```plist
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
// highlight-start
4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */,
4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */,
// highlight-end
5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */,
13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */,
```
4) Replace `App.js` with the following:
```tsx title="App.js"
import React, { useState, type Node } from 'react';
import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
import { read, utils, version } from 'xlsx';
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
const DocumentPicker = getEnforcing('DocumentPicker');
const App: () => Node = () => {
const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]);
return (
<SafeAreaView style={styles.outer}>
<Text style={styles.title}>SheetJS × React Native MacOS {version}</Text>
<TouchableHighlight onPress={async() => {
try {
const b64 = await DocumentPicker.PickAndRead();
const wb = read(b64);
setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } ));
} catch(err) { alert(`Error: ${err.message}`); }
}}><Text style={styles.button}>Click here to Open File!</Text></TouchableHighlight>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={styles.table}>{aoa.map((row,R) => (
<View style={styles.row} key={R}>{row.map((cell,C) => (
<View style={styles.cell} key={C}><Text>{cell}</Text></View>
))}</View>
))}</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
cell: { flex: 4 },
row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', },
table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', },
outer: { marginTop: 32, paddingHorizontal: 24, },
title: { fontSize: 24, fontWeight: '600', },
button: { marginTop: 8, fontSize: 18, fontWeight: '400', },
});
export default App;
```
5) Test the app:
```bash
npx react-native run-macos
```
Download <https://sheetjs.com/pres.xlsx>, then click on "open file". Use the
file picker to select the `pres.xlsx` file and the app will show the data.
6) Make a release build:
```bash
xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release
```

@ -0,0 +1,4 @@
{
"label": "Desktop Applications",
"position": 3
}

@ -0,0 +1,37 @@
---
title: Desktop Applications
pagination_prev: demos/mobile
pagination_next: demos/grid
---
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
Web technologies like JavaScript and HTML have been adapted to the traditional
app space. Typically these frameworks bundle a JavaScript engine as well as a
windowing framework. SheetJS is compatible with many app frameworks.
Demos for common tools are included in separate pages:
<ul>{useCurrentSidebarCategory().items.map((item, index) => {
const listyle = (item.customProps?.icon) ? {
listStyleImage: `url("${item.customProps.icon}")`
} : {};
return (<li style={listyle} {...(item.customProps?.class ? {className: item.customProps.class}: {})}>
<a href={item.href}>{item.label}</a>{item.customProps?.summary && (" - " + item.customProps.summary)}
</li>);
})}</ul>
:::note Recommendation
Electron is the most established and widely-used framework. With deep support
for NodeJS modules and consistent user interfaces, it is the recommended choice
for new projects and for web developers.
Frameworks like Wails are compelling alternatives for teams with experience in
other programming languages.
Frameworks like React Native generate applications that use native UI elements.
:::

@ -1,5 +1,6 @@
---
title: Data Grids and Tables
pagination_prev: demos/desktop/index
---
Various JavaScript UI components provide a more interactive editing experience.

@ -19,7 +19,7 @@ run in the web browser, demos will include interactive examples.
- [`Web SQL Database`](/docs/demos/database#websql)
- [`IndexedDB`](/docs/demos/database#indexeddb)
### Frameworks
### Web Frameworks
- [`Angular`](/docs/demos/angular)
- [`React`](/docs/demos/react)
@ -38,6 +38,15 @@ run in the web browser, demos will include interactive examples.
- [`angular-ui-grid`](/docs/demos/grid#angular-ui-grid)
- [`material ui`](/docs/demos/grid#material-ui-table)
### Desktop App Frameworks
- [`Electron`](/docs/demos/desktop/electron)
- [`NW.js`](/docs/demos/desktop/nwjs)
- [`Wails`](/docs/demos/desktop/wails)
- [`Tauri`](/docs/demos/desktop/tauri)
- [`NeutralinoJS`](/docs/demos/desktop/neutralino)
- [`React Native for Desktop`](/docs/demos/desktop/reactnative)
### Platforms and Integrations
- [`Command-Line Tools`](/docs/demos/cli)
@ -45,10 +54,6 @@ run in the web browser, demos will include interactive examples.
- [`NodeJS Server-Side Processing`](/docs/demos/server#nodejs)
- [`Deno Server-Side Processing`](/docs/demos/server#deno)
- [`Content Management and Static Sites`](/docs/demos/content)
- [`Electron`](/docs/demos/desktop#electron)
- [`NW.js`](/docs/demos/desktop#nwjs)
- [`Tauri`](/docs/demos/desktop#tauri)
- [`Wails`](/docs/demos/desktop#wails)
- [`Chrome and Chromium Extensions`](/docs/demos/chromium)
- [`Google Sheets API`](/docs/demos/gsheet)
- [`ExtendScript for Adobe Apps`](/docs/demos/extendscript)

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB