This commit is contained in:
SheetJS 2023-01-05 18:33:49 -05:00
parent 3616b04348
commit e27218f52a
21 changed files with 1816 additions and 1720 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,647 @@
---
title: React Native
pagination_prev: demos/salesforce
pagination_next: demos/desktop/index
sidebar_position: 1
sidebar_custom_props:
summary: React + Native Rendering
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main `App.js` entrypoint or any script in the project.
The "Complete Example" creates an app that looks like the screenshots below:
<table><thead><tr>
<th><a href="#demo">iOS</a></th>
<th><a href="#demo">Android</a></th>
</tr></thead><tbody><tr><td>
![iOS screenshot](pathname:///mobile/rnios3.png)
</td><td>
![Android screenshot](pathname:///mobile/rnand3.png)
</td></tr></tbody></table>
## Native Libraries
:::warning
React Native does not provide a native file picker or a method for reading and
writing data from documents on the devices. A third-party library must be used.
Since React Native internals change between releases, libraries may only work
with specific versions of React Native. Project documentation should be
consulted before picking a library.
:::
The following table lists tested file plugins. "OS" lists tested platforms
("A" for Android and "I" for iOS). "Copy" indicates whether an explicit copy
is needed (file picker copies to cache directory and file plugin reads cache).
| File system Plugin | File Picker Plugin | OS | Copy |
|:---------------------------|:-------------------------------|:----:|:-----|
| `react-native-file-access` | `react-native-document-picker` | `AI` | |
| `react-native-blob-util` | `react-native-document-picker` | `AI` | YES |
| `rn-fetch-blob` | `react-native-document-picker` | `AI` | YES |
| `react-native-fs` | `react-native-document-picker` | `AI` | YES |
| `expo-file-system` | `expo-document-picker` | ` I` | YES |
### RN File Picker
The following libraries have been tested:
#### `react-native-document-picker`
<details open><summary><b>Selecting a file</b> (click to show)</summary>
When a copy is not needed:
```js
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
const path = f.uri; // this path can be read by RN file plugins
```
When a copy is needed:
```js
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
const path = f.fileCopyUri; // this path can be read by RN file plugins
```
</details>
#### `expo-document-picker`
<details><summary><b>Selecting a file</b> (click to show)</summary>
When using `DocumentPicker.getDocumentAsync`, enable `copyToCacheDirectory`:
```js
import * as DocumentPicker from 'expo-document-picker';
const result = await DocumentPicker.getDocumentAsync({
// highlight-next-line
copyToCacheDirectory: true,
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
});
const path = result.uri;
```
</details>
### RN File Plugins
The following libraries have been tested:
#### `react-native-blob-util` and `rn-fetch-blob`
:::note Historical Context
The `react-native-fetch-blob` project was archived in 2019. At the time, there
were a number of project forks. The maintainers blessed the `rn-fetch-blob`
fork as the spiritual successor.
`react-native-blob-util` is an active fork of `rn-fetch-blob`
On the day that this demo was tested (2022 August 14), both `rn-fetch-blob` and
`react-native-blob-util` worked with the tested iOS and Android SDK versions.
The APIs are identical for the purposes of working with files.
:::
The `ascii` type returns an array of numbers corresponding to the raw bytes.
A `Uint8Array` from the data is compatible with the `buffer` type.
<details open><summary><b>Reading and Writing snippets</b> (click to show)</summary>
The snippets use `rn-fetch-blob`. To use `react-native-blob-util`, change the
`import` statements to load the module.
_Reading Data_
```js
import * as XLSX from "xlsx";
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { readFile } = RNFetchBlob.fs;
const res = await readFile(path, 'ascii');
const wb = XLSX.read(new Uint8Array(res), {type:'buffer'});
```
:::caution
On iOS, the URI from `react-native-document-picker` must be massaged:
```js
import { pickSingle } from 'react-native-document-picker';
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs;
const f = await pickSingle({
// highlight-start
// Instruct the document picker to copy file to Documents directory
copyTo: "documentDirectory",
// highlight-end
allowMultiSelection: false, mode: "open" });
// highlight-start
// `f.uri` is the original path and `f.fileCopyUri` is the path to the copy
let path = f.fileCopyUri;
// iOS workaround
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/");
// highlight-end
const res = await readFile(path, 'ascii');
```
:::
_Writing Data_
```js
import * as XLSX from "xlsx";
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs;
const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"});
const file = DocumentDir + "/sheetjsw.xlsx";
const res = await writeFile(file, Array.from(wbout), 'ascii');
```
</details>
#### `react-native-file-access`
The `base64` encoding returns strings compatible with the `base64` type:
<details open><summary><b>Reading and Writing snippets</b> (click to show)</summary>
_Reading Data_
```js
import * as XLSX from "xlsx";
import { FileSystem } from "react-native-file-access";
const b64 = await FileSystem.readFile(path, "base64");
/* b64 is a Base64 string */
const workbook = XLSX.read(b64, {type: "base64"});
```
_Writing Data_
```js
import * as XLSX from "xlsx";
import { Dirs, FileSystem } from "react-native-file-access";
const DDP = Dirs.DocumentDir + "/";
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64");
```
</details>
#### `react-native-fs`
The `ascii` encoding returns binary strings compatible with the `binary` type:
<details open><summary><b>Reading and Writing snippets</b> (click to show)</summary>
_Reading Data_
```js
import * as XLSX from "xlsx";
import { readFile } from "react-native-fs";
const bstr = await readFile(path, "ascii");
/* bstr is a binary string */
const workbook = XLSX.read(bstr, {type: "binary"});
```
_Writing Data_
```js
import * as XLSX from "xlsx";
import { writeFile, DocumentDirectoryPath } from "react-native-fs";
const bstr = XLSX.write(workbook, {type:'binary', bookType:"xlsx"});
/* bstr is a binary string */
await writeFile(DocumentDirectoryPath + "/sheetjs.xlsx", bstr, "ascii");
```
</details>
#### `expo-file-system`
:::caution
Some Expo APIs return URI that cannot be read with `expo-file-system`. This
will manifest as an error:
> Unsupported scheme for location '...'
The [`expo-document-picker`](#expo-document-picker) snippet makes a local copy.
:::
The `EncodingType.Base64` encoding is compatible with `base64` type.
<details><summary><b>Reading and Writing snippets</b> (click to show)</summary>
_Reading Data_
Calling `FileSystem.readAsStringAsync` with `FileSystem.EncodingType.Base64`
encoding returns a promise resolving to a string compatible with `base64` type:
```js
import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
const workbook = XLSX.read(b64, { type: "base64" });
```
_Writing Data_
The `FileSystem.EncodingType.Base64` encoding accepts Base64 strings:
```js
import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 });
```
</details>
## Demo
:::note
This demo was tested on an Intel Mac on 2022 August 14 with RN `0.67.2`.
The iOS simulator runs iOS 15.5 on an iPhone 13.
The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5.
:::
:::warning
There are many moving parts and pitfalls with React Native apps. It is strongly
recommended to follow the official React Native tutorials for iOS and Android
before approaching this demo. Details like creating an Android Virtual Device
are not covered here.
:::
This example tries to separate the library-specific functions.
0) **Follow the official React Native CLI Guide!**
Development Environment Guide: <https://reactnative.dev/docs/environment-setup>
Follow the instructions for iOS and for Android. They will cover installation
and system configuration. By the end, you should be able to run the sample app
in the Android and the iOS simulators.
1) Create project:
```
npx react-native init SheetJSRN --version="0.67.2"
```
2) Install shared dependencies:
```bash
cd SheetJSRN
curl -LO https://oss.sheetjs.com/assets/img/logo.png
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
npm i -S react-native-table-component react-native-document-picker
```
Refresh iOS project by running `pod install` from the `ios` subfolder:
```bash
cd ios
pod install
cd ..
```
3) Download [`index.js`](pathname:///mobile/index.js) and replace:
```bash
curl -LO https://docs.sheetjs.com/mobile/index.js
```
Start the iOS emulator:
```bash
npx react-native run-ios
```
You should see the skeleton app:
![React Native iOS App](pathname:///mobile/rnios1.png)
4) Pick a filesystem library for integration:
<Tabs>
<TabItem value="RNBU" label="RNBU">
Install `react-native-blob-util` dependency:
```bash
npm i -S react-native-blob-util
```
Add the highlighted lines to `index.js`:
```js title="index.js"
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
async function pickAndParse() {
/* rn-fetch-blob / react-native-blob-util need a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await RNFetchBlob.fs.readFile(path, 'ascii');
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
return file;
}
// highlight-end
const make_width = ws => {
```
</TabItem>
<TabItem value="RNFA" label="RNFA">
Install `react-native-file-access` dependency:
```bash
npm i -S react-native-file-access
```
Add the highlighted lines to `index.js`:
```js title="index.js"
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Dirs, FileSystem } from 'react-native-file-access';
async function pickAndParse() {
/* react-native-file-access does not need a copy */
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
const res = await FileSystem.readFile(f.uri, "base64");
return read(res, {type: 'base64'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
const file = Dirs.DocumentDir + "/sheetjsw.xlsx";
await FileSystem.writeFile(file, wbout, "base64");
return file;
}
// highlight-end
const make_width = ws => {
```
</TabItem>
<TabItem value="RNFB" label="RNFB">
Install `rn-fetch-blob` dependency:
```bash
npm i -S rn-fetch-blob
```
Add the highlighted lines to `index.js`:
```js title="index.js"
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
async function pickAndParse() {
/* rn-fetch-blob / react-native-blob-util need a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await RNFetchBlob.fs.readFile(path, 'ascii');
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
return file;
}
// highlight-end
const make_width = ws => {
```
</TabItem>
<TabItem value="RNFS" label="RNFS">
Install `react-native-fs` dependency:
```bash
npm i -S react-native-fs
```
Add the highlighted lines to `index.js`:
```js title="index.js"
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs';
async function pickAndParse() {
/* react-native-fs needs a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
const bstr = await readFile(f.fileCopyUri, 'ascii');
return read(bstr, {type:'binary'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'binary', bookType:"xlsx"});
const file = DocumentDirectoryPath + "/sheetjsw.xlsx";
await writeFile(file, wbout, 'ascii');
return file;
}
// highlight-end
const make_width = ws => {
```
</TabItem>
<TabItem value="EXPO" label="EXPO">
:::caution
At the time of testing, the `npx install-expo-modules` step breaks the Android
project. The demo works as expected on iOS.
:::
Install `expo-file-system` and `expo-document-picker` dependencies:
```bash
npx install-expo-modules
npm i -S expo-file-system expo-document-picker
```
Add the highlighted lines to `index.js`:
```js title="index.js"
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { getDocumentAsync } from 'expo-document-picker';
import { documentDirectory, readAsStringAsync, writeAsStringAsync } from 'expo-file-system';
async function pickAndParse() {
const result = await getDocumentAsync({copyToCacheDirectory: true});
const path = result.uri;
const res = await readAsStringAsync(path, { encoding: "base64" });
return read(res, {type: 'base64'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
const file = documentDirectory + "sheetjsw.xlsx";
await writeAsStringAsync(file, wbout, { encoding: "base64" });
return file;
}
// highlight-end
const make_width = ws => {
```
</TabItem>
</Tabs>
5) Refresh the app:
```bash
cd ios
pod install
cd ..
```
Once refreshed, the development process must be restarted:
```bash
npx react-native run-ios
```
**iOS Testing**
The app can be tested with the following sequence in the simulator:
- Download <https://sheetjs.com/pres.numbers>
- In the simulator, click the Home icon to return to the home screen
- Click on the "Files" icon
- Click and drag `pres.numbers` from a Finder window into the simulator.
![save file iOS](pathname:///mobile/quasar7a.png)
- Make sure "On My iPhone" is highlighted and select "Save"
- Click the Home icon again then select the `SheetJSRN` app
- Click "Import data" and select `pres`:
![pick file iOS](pathname:///mobile/rnios2.png)
Once selected, the screen should refresh with new contents:
![read file iOS](pathname:///mobile/rnios3.png)
- Click "Export data". You will see a popup with a location:
![write file iOS](pathname:///mobile/rnios4.png)
- Find the file and verify the contents are correct:
```bash
find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
```
Once testing is complete, stop the simulator and the development process.
**Android Testing**
There are no Android-specific steps. Emulator can be started with:
```bash
npx react-native run-android
```
![React Native Android App](pathname:///mobile/rnand1.png)
The app can be tested with the following sequence in the simulator:
- Download <https://sheetjs.com/pres.numbers>
- Click and drag `pres.numbers` from a Finder window into the simulator.
- Click "Import data" and select `pres.numbers`:
![pick file Android](pathname:///mobile/rnand2.png)
Once selected, the screen should refresh with new contents:
![read file Android](pathname:///mobile/rnand3.png)
- Click "Export data". You will see a popup with a location:
![write file Android](pathname:///mobile/rnand4.png)
- Pull the file from the simulator and verify the contents:
```bash
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
npx xlsx-cli /tmp/sheetjsw.xlsx
```

View File

@ -0,0 +1,350 @@
---
title: NativeScript
pagination_prev: demos/salesforce
pagination_next: demos/desktop/index
sidebar_position: 2
sidebar_custom_props:
summary: JS + Native Elements
---
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main entrypoint or any script in the project.
:::warning Binary Data issues
NativeScript will not safely transmit binary or UTF-8 strings. XLSB, NUMBERS,
XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled.
This is a known NativeScript bug.
This demo will focus on ASCII CSV files. Once the bug is resolved, XLSX and
other formats will be supported.
:::
## Integration Details
The `@nativescript/core/file-system` package provides classes for file access.
Reading and writing data require a file handle. The following snippet searches
typical document folders for a specified filename:
```ts
import { File, Folder, knownFolders, path } from '@nativescript/core/file-system';
function get_handle_for_filename(filename: string): File {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
const url: string = path.normalize(target.path + "///" + filename);
return File.fromPath(url);
}
```
The encoding `ISO_8859_1` spiritually resembles the `"binary"` SheetJS type
**Reading data**
`File#readText(encoding.ISO_8859_1)` returns strings compatible with `"binary"`
```ts
/* get binary string */
const bstr: string = await file.readText(encoding.ISO_8859_1);
/* read workbook */
const wb = read(bstr, { type: "binary" });
```
**Writing data**
`File#writeText` with the `ISO_8859_1` encoding accepts `"binary"` strings with
the caveat listed in the warning at the top of this section:
```ts
/* generate binary string */
const bstr: string = write(wb, { bookType: 'csv', type: 'binary' });
/* attempt to save binary string to file */
await file.writeText(bstr, encoding.ISO_8859_1);
```
## Demo
The demo builds off of the NativeScript + Angular example. Familiarity with
Angular and TypeScript is assumed.
:::note
This demo was tested on an Intel Mac on 2022 August 10. NativeScript version
(as verified with `ns --version`) is `8.3.2`. The iOS simulator runs iOS 15.5
on an iPhone SE 3rd generation.
:::
<details><summary><b>Complete Example</b> (click to show)</summary>
0) Follow the official Environment Setup instructions (tested with "MacOS + iOS")
1) Create a skeleton NativeScript + Angular app:
```bash
ns create SheetJSNS --ng
```
2) Launch the app in the iOS simulator to verify that the demo built properly:
```bash
cd SheetJSNS
ns run ios
```
(this may take a while)
Once the simulator launches and the test app is displayed, end the script by
selecting the terminal and entering the key sequence `CTRL + C`
3) From the project folder, install the library:
```bash
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
4) To confirm the library was loaded, change the title to show the version. The
differences are highlighted.
`src/app/item/items.component.ts` imports the version string to the component:
```ts title="src/app/item/items.component.ts"
// highlight-next-line
import { version } from 'xlsx';
import { Component, OnInit } from '@angular/core'
import { Item } from './item'
import { ItemService } from './item.service'
@Component({
selector: 'ns-items',
templateUrl: './items.component.html',
})
export class ItemsComponent implements OnInit {
items: Array<Item>
// highlight-next-line
version = `SheetJS - ${version}`;
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.items = this.itemService.getItems()
}
}
```
`src/app/item/items.component.html` references the version in the title:
```xml title="src/app/item/items.component.html"
<!-- highlight-next-line -->
<ActionBar [title]="version"></ActionBar>
<GridLayout>
<ListView [items]="items">
<ng-template let-item="item">
<StackLayout [nsRouterLink]="['/item', item.id]">
<Label [text]="item.name"></Label>
</StackLayout>
</ng-template>
</ListView>
</GridLayout>
```
Relaunch the app with `ns run ios` and the title bar should show the version.
![NativeScript Step 4](pathname:///mobile/nativescript4.png)
5) Add the Import and Export buttons to the template:
```xml title="src/app/item/items.component.html"
<ActionBar [title]="version"></ActionBar>
<!-- highlight-start -->
<StackLayout>
<StackLayout orientation="horizontal">
<Button text="Import File" (tap)="import()" style="padding: 10px"></Button>
<Button text="Export File" (tap)="export()" style="padding: 10px"></Button>
</StackLayout>
<!-- highlight-end -->
<ListView [items]="items">
<ng-template let-item="item">
<StackLayout [nsRouterLink]="['/item', item.id]">
<Label [text]="item.name"></Label>
</StackLayout>
</ng-template>
</ListView>
<!-- highlight-next-line -->
</StackLayout>
```
```ts title="src/app/item/items.component.ts"
// highlight-start
import { version, utils, read, write } from 'xlsx';
import { Dialogs } from '@nativescript/core';
import { encoding } from '@nativescript/core/text';
import { File, Folder, knownFolders, path } from '@nativescript/core/file-system';
// highlight-end
import { Component, OnInit } from '@angular/core'
import { Item } from './item'
import { ItemService } from './item.service'
// highlight-start
function get_handle_for_filename(filename: string): [File, string] {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
const url: string = path.normalize(target.path + "///" + filename);
return [File.fromPath(url), url];
}
// highlight-end
@Component({
selector: 'ns-items',
templateUrl: './items.component.html',
})
export class ItemsComponent implements OnInit {
items: Array<Item>
version: string = `SheetJS - ${version}`;
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.items = this.itemService.getItems()
}
// highlight-start
/* Import button */
async import() {
}
/* Export button */
async export() {
}
// highlight-end
}
```
Restart the app process and two buttons should show up at the top:
![NativeScript Step 5](pathname:///mobile/nativescript5.png)
6) Implement import and export:
```ts title="src/app/item/items.component.ts"
import { version, utils, read, write } from 'xlsx';
import { Dialogs } from '@nativescript/core';
import { encoding } from '@nativescript/core/text';
import { File, Folder, knownFolders, path } from '@nativescript/core/file-system';
import { Component, OnInit } from '@angular/core'
import { Item } from './item'
import { ItemService } from './item.service'
function get_handle_for_filename(filename: string): [File, string] {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
const url: string = path.normalize(target.path + "///" + filename);
return [File.fromPath(url), url];
}
@Component({
selector: 'ns-items',
templateUrl: './items.component.html',
})
export class ItemsComponent implements OnInit {
items: Array<Item>
version: string = `SheetJS - ${version}`;
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.items = this.itemService.getItems()
}
/* Import button */
async import() {
// highlight-start
/* find appropriate path */
const [file, url] = get_handle_for_filename("SheetJSNS.csv");
try {
/* get binary string */
const bstr: string = await file.readText(encoding.ISO_8859_1);
/* read workbook */
const wb = read(bstr, { type: "binary" });
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* update table */
this.items = utils.sheet_to_json<Item>(ws);
Dialogs.alert(`Attempting to read to ${filename} in ${url}`);
} catch(e) { Dialogs.alert(e.message); }
// highlight-end
}
/* Export button */
async export() {
// highlight-start
/* find appropriate path */
const [file, url] = get_handle_for_filename("SheetJSNS.csv");
try {
/* create worksheet from data */
const ws = utils.json_to_sheet(this.items);
/* create workbook from worksheet */
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Sheet1");
/* generate binary string */
const wbout: string = write(wb, { bookType: 'csv', type: 'binary' });
/* attempt to save binary string to file */
await file.writeText(wbout, encoding.ISO_8859_1);
Dialogs.alert(`Wrote to ${filename} in ${url}`);
} catch(e) { Dialogs.alert(e.message); }
// highlight-end
}
}
```
Restart the app process.
**Testing**
The app can be tested with the following sequence in the simulator:
- Hit "Export File". A dialog will print where the file was written
- Open that file with a text editor. It will be a 3-column CSV:
```csv
id,name,role
1,Ter Stegen,Goalkeeper
3,Piqué,Defender
4,I. Rakitic,Midfielder
...
```
After the header row, add the line `0,SheetJS,Library`:
```csv
id,name,role
0,SheetJS,Library
1,Ter Stegen,Goalkeeper
3,Piqué,Defender
...
```
- Hit "Import File". A dialog will print the path of the file that was read.
The first item in the list will change:
![NativeScript Step 7](pathname:///mobile/nativescript7.png)
</details>

View File

@ -0,0 +1,381 @@
---
title: Quasar
pagination_prev: demos/salesforce
pagination_next: demos/desktop/index
sidebar_position: 3
sidebar_custom_props:
summary: VueJS + Web View
---
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main entrypoint or any script in the project.
This demo will use the Quasar ViteJS starter project with VueJS and Cordova.
### Integration Details
The complete solution uses `cordova-plugin-file` for file operations. It can
be installed like any other Cordova plugin:
```bash
cd src-cordova
cordova plugin add cordova-plugin-file
cd ..
```
#### Reading data
The `q-file` component presents an API reminiscent of File Input elements:
```html
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
```
When binding to the `input` element, the callback receives an `Event` object:
```ts
import { read } from 'xlsx';
// assuming `todos` is a standard VueJS `ref`
async function updateFile(v) { try {
// `v.target.files[0]` is the desired file object
const files = (v.target as HTMLInputElement).files;
if(!files || files.length == 0) return;
// read first file
const wb = read(await files[0].arrayBuffer());
// get data of first worksheet as an array of objects
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
// update state
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
} catch(e) { console.log(e); } }
```
#### Writing data
The API is shaped like the File and Directory Entries API. For clarity, since
the code is a "pyramid of doom", the error handlers are omitted:
```ts
import { write } from 'xlsx';
// on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array`
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
// Request filesystem access for persistent storage
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
// Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
// Request a FileWriter for writing data
entry.createWriter(writer => {
// The FileWriter API needs an actual Blob
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
// This callback is called if the write is successful
writer.onwriteend = () => {
// TODO: show a dialog
};
// writer.onerror will be invoked if there is an error in writing
// write the data
writer.write(data);
});
});
});
```
### Demo
:::note
This demo was tested on an Intel Mac on 2022 August 14. Quasar version `2.7.7`.
The iOS simulator runs iOS 15.5 on an iPhone SE 3rd generation.
:::
The demo draws from the ViteJS example. Familiarity with VueJS and TypeScript
is assumed.
<details><summary><b>Complete Example</b> (click to show)</summary>
0) Ensure all of the dependencies are installed. Install the CLI globally:
```bash
npm i -g @quasar/cli cordova
```
(you may need to run `sudo npm i -g` if there are permission issues)
1) Create a new app:
```bash
npm init quasar
```
<!-- spellchecker-disable -->
When prompted:
- "What would you like to build?": `App with Quasar CLI`
- "Project folder": `SheetJSQuasar`
- "Pick Quasar version": `Quasar v2 (Vue 3 | latest and greatest)`
- "Pick script type": `Typescript`
- "Pick Quasar App CLI variant": `Quasar App CLI with Vite`
- "Package name": (just press enter, it will use the default `sheetjsquasar`
- "Project product name": `SheetJSQuasar`
- "Project description": `SheetJS + Quasar`
- "Author": (just press enter, it will use your git config settings)
- "Pick a Vue component style": `Composition API`
- "Pick your CSS preprocessor": `None`
- "Check the features needed for your project": Deselect everything
- "Install project dependencies": `No`
2) Install dependencies:
<!-- spellchecker-enable -->
```bash
cd SheetJSQuasar
npm i
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
3) Set up Cordova:
```bash
quasar mode add cordova
```
When prompted, enter the app id `org.sheetjs.quasar`.
It will create a new `src-cordova` folder. Continue in that folder:
```bash
cd src-cordova
cordova platform add ios
cordova plugin add cordova-plugin-wkwebview-engine
cordova plugin add cordova-plugin-file
```
:::note
If there is an error `Could not load API for iOS project`, it needs to be reset:
```bash
cordova platform rm ios
cordova platform add ios
cordova plugin add cordova-plugin-file
```
:::
Return to the project directory:
```bash
cd ..
```
4) Start the development server:
```bash
quasar dev -m ios
```
:::caution
If the app is blank or not refreshing, delete the app and close the simulator,
then restart the development process.
:::
![Quasar Step 4](pathname:///mobile/quasar4.png)
5) Add the Dialog plugin to `quasar.config.js`:
```js title="quasar.config.js"
// Quasar plugins
// highlight-next-line
plugins: ['Dialog']
```
6) In the template section of `src/pages/IndexPage.vue`, add a Save button and
a Load file picker component at the bottom of the page:
```html title="src/pages/IndexPage.vue"
<!-- highlight-start -->
<q-btn-group>
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
<q-btn label="Save File" @click="saveFile" />
</q-btn-group>
<!-- highlight-end -->
</q-page>
</template>
```
This uses two functions that should be added to the component script:
```ts title="src/pages/IndexPage.vue"
const meta = ref<Meta>({
totalCount: 1200
});
// highlight-start
function saveFile() {
}
async function updateFile(v) {
}
return { todos, meta, saveFile, updateFile };
// highlight-end
}
});
```
The app should now show two buttons at the bottom:
![Quasar Step 6](pathname:///mobile/quasar6.png)
:::caution
If the app is blank or not refreshing, delete the app and close the simulator,
then restart the development process.
:::
7) Wire up the `updateFile` function:
```ts title="src/pages/IndexPage.vue"
import { defineComponent, ref } from 'vue';
// highlight-start
import { read, write, utils } from 'xlsx';
import { useQuasar } from 'quasar';
// highlight-end
export default defineComponent({
// ...
// highlight-start
const $q = useQuasar();
function dialogerr(e) { $q.dialog({title: "Error!", message: e.message || String(e)}); }
// highlight-end
function saveFile() {
}
async function updateFile(v) {
// highlight-start
try {
const files = (v.target as HTMLInputElement).files;
if(!files || files.length == 0) return;
const wb = read(await files[0].arrayBuffer());
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
} catch(e) { dialogerr(e); }
// highlight-end
}
```
To test that reading works:
- Download <https://sheetjs.com/pres.numbers>
- In the simulator, click the Home icon to return to the home screen
- Click on the "Files" icon
- Click and drag `pres.numbers` from a Finder window into the simulator.
![Quasar Step 7 save file](pathname:///mobile/quasar7a.png)
- Make sure "On My iPhone" is highlighted and select "Save"
- Click the Home icon again then select the `SheetJSQuasar` app
- Click the "Load" button, then select "Choose File" and select `pres`:
![Quasar Step 7 load file](pathname:///mobile/quasar7b.png)
Once selected, the screen should refresh with new contents:
![Quasar Step 7 new data](pathname:///mobile/quasar7c.png)
8) Wire up the `saveFile` function:
```js
function saveFile() {
// highlight-start
/* generate workbook from state */
const ws = utils.json_to_sheet(todos.value);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "SheetJSQuasar");
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
/* save to file */
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
try {
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
const msg = `File stored at ${$q.cordova.file.documentsDirectory} ${entry.fullPath}`;
entry.createWriter(writer => {
try {
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
writer.onwriteend = () => {
try {
$q.dialog({title: "Success!", message: msg});
} catch(e) { dialogerr(e); }
};
writer.onerror = dialogerr;
writer.write(data);
} catch(e) { dialogerr(e); }
}, dialogerr);
}, dialogerr);
} catch(e) { dialogerr(e) }
}, dialogerr);
// highlight-end
}
```
The page should revert to the old contents.
To test that writing works:
- Click "Save File". You will see a popup with a location:
![Quasar Step 8](pathname:///mobile/quasar8.png)
- Find the file and verify the contents are correct. Run in a new terminal:
```bash
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
```
Since the contents reverted, you should see
```
SheetJSQuasar
id,content
1,ct1
2,ct2
3,ct3
4,ct4
5,ct5
```
- Use "Load File" to select `pres.numbers` again. Wait for the app to refresh.
- Click "Save File", then re-run the command:
```bash
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
```
The contents from `pres.numbers` should show up now, with a new header row:
```
SheetJSQuasar
id,content
42,Bill Clinton
43,GeorgeW Bush
44,Barack Obama
45,Donald Trump
46,Joseph Biden
```
</details>

View File

@ -0,0 +1,155 @@
---
title: Ionic
pagination_prev: demos/salesforce
pagination_next: demos/desktop/index
sidebar_position: 4
sidebar_custom_props:
summary: Native Components + Web View
---
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main entrypoint or any script in the project.
:::warning Telemetry
Before starting this demo, manually disable telemetry. On Linux and MacOS:
```bash
rm -rf ~/.ionic/
mkdir ~/.ionic
cat <<EOF > ~/.ionic/config.json
{
"version": "6.20.1",
"telemetry": false,
"npmClient": "npm"
}
EOF
npx @capacitor/cli telemetry off
```
To verify telemetry was disabled:
```bash
npx @ionic/cli config get -g telemetry
npx @capacitor/cli telemetry
```
:::
## Cordova
:::caution
The latest version of Ionic uses CapacitorJS. These notes are for Cordova apps.
:::
`Array<Array<any>>` neatly maps to a table with `ngFor`:
```html
<ion-grid>
<ion-row *ngFor="let row of data">
<ion-col *ngFor="let val of row">
{{val}}
</ion-col>
</ion-row>
</ion-grid>
```
`@ionic-native/file` reads and writes files on devices. `readAsArrayBuffer`
returns `ArrayBuffer` objects suitable for `array` type, and `array` type can
be converted to blobs that can be exported with `writeFile`:
```ts
/* read a workbook */
const ab: ArrayBuffer = await this.file.readAsArrayBuffer(url, filename);
const wb: XLSX.WorkBook = XLSX.read(bstr, {type: 'array'});
/* write a workbook */
const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
let blob = new Blob([wbout], {type: 'application/octet-stream'});
this.file.writeFile(url, filename, blob, {replace: true});
```
## Demo
:::note
This demo was tested on an Intel Mac on 2022 August 18 with Cordova.
The file integration uses `@ionic-native/file` version `5.36.0`.
The iOS simulator runs iOS 15.5 on an iPod Touch 7th Gen.
:::
<details><summary><b>Complete Example</b> (click to show)</summary>
0) Disable telemetry as noted in the warning.
Install required global dependencies:
```bash
npm i -g cordova-res @angular/cli native-run
```
Follow the [React Native demo](#demo) to ensure iOS and Android sims are ready.
1) Create a new project:
```bash
npx @ionic/cli start SheetJSIonic blank --type angular --cordova --quiet --no-git --no-link --confirm
```
If a prompt discusses Cordova and Capacitor, enter `Yes` to continue.
If a prompt asks about creating an Ionic account, enter `N` to opt out.
2) Set up Cordova:
```bash
npx @ionic/cli cordova platform add ios --confirm
npx @ionic/cli cordova plugin add cordova-plugin-file
npm install --save @ionic-native/core @ionic-native/file @ionic/cordova-builders
```
3) Install dependencies:
```bash
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
4) Add `@ionic-native/file` to the module. Differences highlighted below:
```ts title="src/app/app.module.ts"
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
// highlight-next-line
import { File } from '@ionic-native/file/ngx';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
// highlight-next-line
providers: [File, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
```
5) Download [`home.page.ts`](pathname:///ionic/home.page.ts) and replace:
```bash
curl -o src/app/home/home.page.ts -L https://docs.sheetjs.com/ionic/home.page.ts
```
6) Test the app:
```bash
npx @ionic/cli cordova emulate ios
```
</details>

View File

@ -0,0 +1,192 @@
---
title: CapacitorJS
pagination_prev: demos/salesforce
pagination_next: demos/desktop/index
sidebar_position: 5
sidebar_custom_props:
summary: JS + Web View
---
## CapacitorJS
:::note
This demo was tested on an Intel Mac on 2022 August 26 with Svelte.
The iOS simulator runs iOS 15.5 on an iPhone 13 Pro Max.
:::
:::warning Telemetry
Before starting this demo, manually disable telemetry. On Linux and MacOS:
```bash
npx @capacitor/cli telemetry off
```
To verify telemetry was disabled:
```bash
npx @capacitor/cli telemetry
```
:::
### Integration Details
This example uses Svelte, but the same principles apply to other frameworks.
#### Reading data
The standard HTML5 File Input element logic works in CapacitorJS:
```html
<script>
import { read, utils } from 'xlsx';
let html = "";
/* show file picker, read file, load table */
async function importFile(evt) {
// highlight-start
const f = evt.target.files[0];
const wb = read(await f.arrayBuffer());
// highlight-end
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
html = utils.sheet_to_html(ws); // generate HTML and update state
}
</script>
<main>
<!-- highlight-next-line -->
<input type="file" on:change={importFile}/>
<div bind:this={tbl}>{@html html}</div>
</main>
```
#### Writing data
`@capacitor/filesystem` can write Base64 strings:
```html
<script>
import { Filesystem, Directory } from '@capacitor/filesystem';
import { utils, writeXLSX } from 'xlsx';
let html = "";
let tbl;
/* get state data and export to XLSX */
async function exportFile() {
const elt = tbl.getElementsByTagName("TABLE")[0];
const wb = utils.table_to_book(elt);
/* generate Base64 string for Capacitor */
// highlight-start
const data = writeXLSX(wb, { type: "base64" });
await Filesystem.writeFile({
data,
path: "SheetJSCap.xlsx",
directory: Directory.Documents
}); // write file
// highlight-end
}
</script>
<main>
<button on:click={exportFile}>Export XLSX</button>
<div bind:this={tbl}>{@html html}</div>
</main>
```
### Demo
<details><summary><b>Complete Example</b> (click to show)</summary>
0) Disable telemetry as noted in the warning.
Follow the [React Native demo](#demo) to ensure iOS and Android sims are ready.
1) Create a new Svelte project:
```bash
npm create vite@latest sheetjs-cap -- --template svelte
cd sheetjs-cap
```
2) Install dependencies:
```bash
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
npm i --save @capacitor/core @capacitor/cli @capacitor/ios @capacitor/filesystem
```
3) Create CapacitorJS structure:
```bash
npx cap init sheetjs-cap com.sheetjs.cap --web-dir=dist
npx cap add ios
```
4) Replace the contents of `src/App.svelte` with the following:
```html title="src/App.svelte"
<script>
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { onMount } from 'svelte';
import { read, utils, version, writeXLSX } from 'xlsx';
let html = "";
let tbl;
/* Fetch and update the state once */
onMount(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
html = utils.sheet_to_html(ws); // generate HTML and update state
});
/* get state data and export to XLSX */
async function exportFile() {
const elt = tbl.getElementsByTagName("TABLE")[0];
const wb = utils.table_to_book(elt);
/* generate Base64 string for Capacitor */
const data = writeXLSX(wb, { type: "base64" });
/* write */
await Filesystem.writeFile({
path: "SheetJSCap.xlsx",
data,
directory: Directory.Documents
});
}
/* show file picker, read file, load table */
async function importFile(evt) {
const f = evt.target.files[0];
const wb = read(await f.arrayBuffer());
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
html = utils.sheet_to_html(ws); // generate HTML and update state
}
</script>
<main>
<h3>SheetJS × CapacitorJS { version }</h3>
<input type="file" on:change={importFile}/>
<button on:click={exportFile}>Export XLSX</button>
<div bind:this={tbl}>{@html html}</div>
</main>
```
5) Test the app:
```bash
npm run build; npx cap sync; npx cap run ios
```
There are 3 steps: build the Svelte app, sync with CapacitorJS, and run sim.
This sequence must be run every time to ensure changes are propagated.
</details>

View File

@ -0,0 +1,4 @@
{
"label": "iOS and Android Apps",
"position": 3
}

View File

@ -0,0 +1,62 @@
---
title: iOS and Android Apps
pagination_prev: demos/salesforce
pagination_next: demos/desktop/index
---
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
Many mobile app frameworks mix JavaScript / CSS / HTML5 concepts with native
extensions and libraries to create a hybrid development experience. Developers
well-versed in web technologies can now build actual mobile applications that
run on iOS and Android!
:::warning
**The ecosystem has broken backwards-compatibility many times!**
iOS and Android, as well as the underlying JavaScript frameworks, make breaking
changes regularly. The demos were tested against emulators / real devices at
some point in time. A framework or OS change can render the demos inoperable.
MacOS is required for the iOS demos. The Android demos were tested on MacOS.
:::
The ["JavaScript Engines"](/docs/demos/engines) section includes samples for JS
engines used in the mobile app frameworks. SheetJS libraries have been tested
in the relevant engines and should "just work" with some caveats.
Demos for common tools are included in separate pages. Each demo section will
mention test dates and platform versions.
<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
React Native is extremely popular and is the recommended choice for greenfield
projects that can use community modules. However, its "lean core" approach
forces developers to learn iOS/Android programming or use community modules to
provide basic app features.
The original Web View framework was PhoneGap/Cordova. The modern frameworks
are built atop Cordova. Cordova is waning in popularity but it has a deep
library of community modules to solve many problems.
Before creating a new app, it is important to identify what features the app
should support and investigate community modules. If there are popular modules
for features that must be included, or for teams that are comfortable with
native app development, React Native is the obvious choice.
NativeScript is not recommended at this time. There are known bugs related to
binary data processing. The demo only supports plaintext file formats like CSV.
:::

View File

@ -1,6 +1,6 @@
---
title: Electron
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
sidebar_position: 1
sidebar_custom_props:

View File

@ -1,6 +1,6 @@
---
title: NW.js
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
sidebar_position: 2
sidebar_custom_props:

View File

@ -1,6 +1,6 @@
---
title: Wails
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
sidebar_position: 3
sidebar_custom_props:

View File

@ -1,6 +1,6 @@
---
title: Tauri
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
sidebar_position: 4
sidebar_custom_props:

View File

@ -1,6 +1,6 @@
---
title: NeutralinoJS
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
sidebar_position: 5
sidebar_custom_props:

View File

@ -1,6 +1,6 @@
---
title: React Native for Desktop
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
sidebar_position: 6
sidebar_custom_props:
@ -13,7 +13,7 @@ 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)
applications, [check the mobile demo](/docs/demos/mobile/reactnative)
:::

View File

@ -1,6 +1,6 @@
---
title: Desktop Applications
pagination_prev: demos/mobile
pagination_prev: demos/mobile/index
pagination_next: demos/grid
---

View File

@ -14,8 +14,8 @@ and TypeScript familiarity is assumed.
Other demos cover general Angular deployments, including:
- [iOS and Android applications powered by NativeScript](/docs/demos/mobile#nativescript)
- [iOS and Android applications powered by Ionic](/docs/demos/mobile#ionic)
- [iOS and Android applications powered by NativeScript](/docs/demos/mobile/nativescript)
- [iOS and Android applications powered by Ionic](/docs/demos/mobile/ionic)
:::warning

View File

@ -10,8 +10,8 @@ familiarity is assumed.
Other demos cover general React deployments, including:
- [Static Site Generation powered by NextJS](/docs/demos/content#nextjs)
- [iOS and Android applications powered by React Native](/docs/demos/mobile#react-native)
- [Desktop application powered by React-Native-Windows](/docs/demos/desktop#react-native-windows)
- [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)

View File

@ -10,8 +10,8 @@ Components (SFC) and VueJS familiarity is assumed.
Other demos cover general VueJS deployments, including:
- [Static Site Generation powered by NuxtJS](/docs/demos/content#nuxtjs)
- [iOS and Android applications powered by Quasar](/docs/demos/mobile#quasar)
- [Desktop application powered by Tauri](/docs/demos/desktop#tauri)
- [iOS and Android applications powered by Quasar](/docs/demos/mobile/quasar)
- [Desktop application powered by Tauri](/docs/demos/desktop/tauri)
- [`vue3-table-lite` UI component](/docs/demos/grid#vue3-table-lite)

View File

@ -9,8 +9,8 @@ familiarity is assumed.
Other demos cover general Svelte deployments, including:
- [iOS applications powered by CapacitorJS](/docs/demos/mobile#capacitorjs)
- [Desktop application powered by Wails](/docs/demos/desktop#wails)
- [iOS applications powered by CapacitorJS](/docs/demos/mobile/capacitor)
- [Desktop application powered by Wails](/docs/demos/desktop/wails)
## Installation

View File

@ -38,6 +38,14 @@ 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)
### iOS and Android Mobile Apps
- [`React Native`](/docs/demos/mobile/reactnative)
- [`NativeScript`](/docs/demos/mobile/nativescript)
- [`Quasar`](/docs/demos/mobile/quasar)
- [`Ionic`](/docs/demos/mobile/ionic)
- [`CapacitorJS`](/docs/demos/mobile/capacitor)
### Desktop App Frameworks
- [`Electron`](/docs/demos/desktop/electron)

View File

@ -5,6 +5,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https:">
<meta name="robots" content="noindex">
<title>SheetJS Electron Demo</title>
<style>
#drop{