forked from sheetjs/docs.sheetjs.com
		
	mobile
This commit is contained in:
		
							parent
							
								
									3616b04348
								
							
						
					
					
						commit
						e27218f52a
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										647
									
								
								docz/docs/03-demos/02-mobile/01-reactnative.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										647
									
								
								docz/docs/03-demos/02-mobile/01-reactnative.md
									
									
									
									
									
										Normal 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>
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
</td><td>
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
</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:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- 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`:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Once selected, the screen should refresh with new contents:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- Click "Export data".  You will see a popup with a location:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- 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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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`:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Once selected, the screen should refresh with new contents:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- Click "Export data".  You will see a popup with a location:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- 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
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										350
									
								
								docz/docs/03-demos/02-mobile/02-nativescript.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										350
									
								
								docz/docs/03-demos/02-mobile/02-nativescript.md
									
									
									
									
									
										Normal 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.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										381
									
								
								docz/docs/03-demos/02-mobile/03-quasar.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										381
									
								
								docz/docs/03-demos/02-mobile/03-quasar.md
									
									
									
									
									
										Normal 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.
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
:::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.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- 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`:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Once selected, the screen should refresh with new contents:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
- 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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										155
									
								
								docz/docs/03-demos/02-mobile/04-ionic.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										155
									
								
								docz/docs/03-demos/02-mobile/04-ionic.md
									
									
									
									
									
										Normal 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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										192
									
								
								docz/docs/03-demos/02-mobile/05-capacitor.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										192
									
								
								docz/docs/03-demos/02-mobile/05-capacitor.md
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										4
									
								
								docz/docs/03-demos/02-mobile/_category_.json
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										4
									
								
								docz/docs/03-demos/02-mobile/_category_.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  "label": "iOS and Android Apps",
 | 
			
		||||
  "position": 3
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								docz/docs/03-demos/02-mobile/index.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										62
									
								
								docz/docs/03-demos/02-mobile/index.md
									
									
									
									
									
										Normal 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.
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
---
 | 
			
		||||
title: Desktop Applications
 | 
			
		||||
pagination_prev: demos/mobile
 | 
			
		||||
pagination_prev: demos/mobile/index
 | 
			
		||||
pagination_next: demos/grid
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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{
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user