2022-08-10 22:11:05 +00:00
|
|
|
---
|
|
|
|
sidebar_position: 19
|
|
|
|
title: iOS and Android Apps
|
|
|
|
---
|
|
|
|
|
2022-08-15 03:07:34 +00:00
|
|
|
import Tabs from '@theme/Tabs';
|
|
|
|
import TabItem from '@theme/TabItem';
|
|
|
|
|
2022-08-10 22:11:05 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
Each demo section will mention test dates and platform versions.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
The ["JavaScript Engines"](./engines) section includes samples for JavaScript
|
|
|
|
engines used in the mobile app frameworks. SheetJS libraries have been tested
|
|
|
|
in the relevant engines and should "just work" with some caveats.
|
|
|
|
|
|
|
|
:::caution readFile and writeFile
|
|
|
|
|
|
|
|
`XLSX.readFile` and `XLSX.writeFile` do not work in mobile apps! The demos
|
|
|
|
include platform-specific details for fetching file data for `XLSX.read` and
|
|
|
|
writing file data generated by `XLSX.write`.
|
|
|
|
|
|
|
|
Some platforms provide this functionality as part of the standard library.
|
|
|
|
Other platforms, including React Native, do not. When the platform does not
|
|
|
|
provide, usually there are third-party modules to provide needed functionality.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
2022-08-15 03:07:34 +00:00
|
|
|
macOS is required for the iOS demos. The Android demos were tested on macOS.
|
|
|
|
|
|
|
|
## React Native
|
|
|
|
|
|
|
|
:::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
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
|
|
| Filesystem 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, URIs 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 URIs 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
|
|
|
|
|
|
|
|
:::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.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
<details open><summary><b>Complete Example</b> (click to show)</summary>
|
|
|
|
|
|
|
|
This example tries to separate the library-specific functions.
|
|
|
|
|
|
|
|
0) **Follow the official React Native CLI Quickstart!**
|
|
|
|
|
|
|
|
Quickstart URL: <http://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 http://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 ..
|
|
|
|
```
|
|
|
|
|
|
|
|
After doing this, the simulator must be stopped and the dev server must reload:
|
|
|
|
|
|
|
|
```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 dev 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
|
|
|
|
```
|
|
|
|
|
|
|
|
</details>
|
|
|
|
|
2022-08-10 22:11:05 +00:00
|
|
|
## NativeScript
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
2022-08-13 22:01:26 +00:00
|
|
|
This demo was tested on an Intel Mac on 2022 August 10. NativeScript version
|
2022-08-10 22:11:05 +00:00
|
|
|
(as verified with `ns --version`) is `8.3.2`. The iOS simulator runs iOS 15.5
|
|
|
|
on an iPhone SE 3rd generation.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
:::warning Binary Data issues
|
|
|
|
|
|
|
|
NativeScript will not safely transmit binary or UTF8 strings. XLSB, NUMBERS,
|
|
|
|
XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled.
|
|
|
|
|
|
|
|
[This is a known NativeScript bug](https://github.com/NativeScript/NativeScript/issues/9586)
|
|
|
|
|
|
|
|
This demo will focus on ASCII CSV files. Once the bug is resolved, XLSX and
|
|
|
|
other formats will be supported.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
The `@nativescript/core/file-system` package provides classes for file access.
|
|
|
|
|
|
|
|
### Integration Details
|
|
|
|
|
|
|
|
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
|
|
|
|
with Angular and TypeScript is assumed.
|
|
|
|
|
2022-08-13 22:01:26 +00:00
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
2022-08-10 22:11:05 +00:00
|
|
|
|
|
|
|
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
|
2022-08-14 08:10:18 +00:00
|
|
|
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
|
2022-08-10 22:11:05 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
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>
|
2022-08-14 08:10:18 +00:00
|
|
|
|
|
|
|
## Quasar
|
|
|
|
|
|
|
|
:::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.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
This demo will focus on VueJS and Cordova with the Quasar Vite starter project.
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
|
|
|
The demo builds off of the Vite example. Familiarity with VueJS and TypeScript
|
|
|
|
is assumed.
|
|
|
|
|
2022-08-15 03:07:34 +00:00
|
|
|
<details><summary><b>Complete Example</b> (click to show)</summary>
|
2022-08-14 08:10:18 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
```
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
```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 dev server:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
quasar dev -m ios
|
|
|
|
```
|
|
|
|
|
|
|
|
:::caution
|
|
|
|
|
|
|
|
If the app is blank, delete the app and close the simulator, then restart dev
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
![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 dev 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>
|
2022-08-19 02:26:17 +00:00
|
|
|
|
|
|
|
## Ionic
|
|
|
|
|
|
|
|
:::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 older apps
|
|
|
|
using Cordova
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
`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});
|
|
|
|
```
|
|
|
|
|