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>
|
||||
|
||||
![iOS screenshot](pathname:///mobile/rnios3.png)
|
||||
|
||||
</td><td>
|
||||
|
||||
![Android screenshot](pathname:///mobile/rnand3.png)
|
||||
|
||||
</td></tr></tbody></table>
|
||||
|
||||
|
||||
## Native Libraries
|
||||
|
||||
:::warning
|
||||
|
||||
React Native does not provide a native file picker or a method for reading and
|
||||
writing data from documents on the devices. A third-party library must be used.
|
||||
|
||||
Since React Native internals change between releases, libraries may only work
|
||||
with specific versions of React Native. Project documentation should be
|
||||
consulted before picking a library.
|
||||
|
||||
:::
|
||||
|
||||
The following table lists tested file plugins. "OS" lists tested platforms
|
||||
("A" for Android and "I" for iOS). "Copy" indicates whether an explicit copy
|
||||
is needed (file picker copies to cache directory and file plugin reads cache).
|
||||
|
||||
| File system Plugin | File Picker Plugin | OS | Copy |
|
||||
|:---------------------------|:-------------------------------|:----:|:-----|
|
||||
| `react-native-file-access` | `react-native-document-picker` | `AI` | |
|
||||
| `react-native-blob-util` | `react-native-document-picker` | `AI` | YES |
|
||||
| `rn-fetch-blob` | `react-native-document-picker` | `AI` | YES |
|
||||
| `react-native-fs` | `react-native-document-picker` | `AI` | YES |
|
||||
| `expo-file-system` | `expo-document-picker` | ` I` | YES |
|
||||
|
||||
### RN File Picker
|
||||
|
||||
The following libraries have been tested:
|
||||
|
||||
#### `react-native-document-picker`
|
||||
|
||||
<details open><summary><b>Selecting a file</b> (click to show)</summary>
|
||||
|
||||
When a copy is not needed:
|
||||
|
||||
```js
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
|
||||
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
|
||||
const path = f.uri; // this path can be read by RN file plugins
|
||||
```
|
||||
|
||||
When a copy is needed:
|
||||
|
||||
```js
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
|
||||
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
|
||||
const path = f.fileCopyUri; // this path can be read by RN file plugins
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### `expo-document-picker`
|
||||
|
||||
<details><summary><b>Selecting a file</b> (click to show)</summary>
|
||||
|
||||
When using `DocumentPicker.getDocumentAsync`, enable `copyToCacheDirectory`:
|
||||
|
||||
```js
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
// highlight-next-line
|
||||
copyToCacheDirectory: true,
|
||||
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
});
|
||||
const path = result.uri;
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### RN File Plugins
|
||||
|
||||
The following libraries have been tested:
|
||||
|
||||
#### `react-native-blob-util` and `rn-fetch-blob`
|
||||
|
||||
:::note Historical Context
|
||||
|
||||
The `react-native-fetch-blob` project was archived in 2019. At the time, there
|
||||
were a number of project forks. The maintainers blessed the `rn-fetch-blob`
|
||||
fork as the spiritual successor.
|
||||
|
||||
`react-native-blob-util` is an active fork of `rn-fetch-blob`
|
||||
|
||||
On the day that this demo was tested (2022 August 14), both `rn-fetch-blob` and
|
||||
`react-native-blob-util` worked with the tested iOS and Android SDK versions.
|
||||
The APIs are identical for the purposes of working with files.
|
||||
|
||||
:::
|
||||
|
||||
The `ascii` type returns an array of numbers corresponding to the raw bytes.
|
||||
A `Uint8Array` from the data is compatible with the `buffer` type.
|
||||
|
||||
<details open><summary><b>Reading and Writing snippets</b> (click to show)</summary>
|
||||
|
||||
The snippets use `rn-fetch-blob`. To use `react-native-blob-util`, change the
|
||||
`import` statements to load the module.
|
||||
|
||||
_Reading Data_
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
|
||||
const { readFile } = RNFetchBlob.fs;
|
||||
|
||||
const res = await readFile(path, 'ascii');
|
||||
const wb = XLSX.read(new Uint8Array(res), {type:'buffer'});
|
||||
```
|
||||
|
||||
:::caution
|
||||
|
||||
On iOS, the URI from `react-native-document-picker` must be massaged:
|
||||
|
||||
```js
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
|
||||
const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs;
|
||||
|
||||
const f = await pickSingle({
|
||||
// highlight-start
|
||||
// Instruct the document picker to copy file to Documents directory
|
||||
copyTo: "documentDirectory",
|
||||
// highlight-end
|
||||
allowMultiSelection: false, mode: "open" });
|
||||
// highlight-start
|
||||
// `f.uri` is the original path and `f.fileCopyUri` is the path to the copy
|
||||
let path = f.fileCopyUri;
|
||||
// iOS workaround
|
||||
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/");
|
||||
// highlight-end
|
||||
|
||||
const res = await readFile(path, 'ascii');
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
_Writing Data_
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
|
||||
const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs;
|
||||
|
||||
const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"});
|
||||
const file = DocumentDir + "/sheetjsw.xlsx";
|
||||
const res = await writeFile(file, Array.from(wbout), 'ascii');
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
#### `react-native-file-access`
|
||||
|
||||
The `base64` encoding returns strings compatible with the `base64` type:
|
||||
|
||||
<details open><summary><b>Reading and Writing snippets</b> (click to show)</summary>
|
||||
|
||||
_Reading Data_
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import { FileSystem } from "react-native-file-access";
|
||||
|
||||
const b64 = await FileSystem.readFile(path, "base64");
|
||||
/* b64 is a Base64 string */
|
||||
const workbook = XLSX.read(b64, {type: "base64"});
|
||||
```
|
||||
|
||||
_Writing Data_
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import { Dirs, FileSystem } from "react-native-file-access";
|
||||
const DDP = Dirs.DocumentDir + "/";
|
||||
|
||||
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
|
||||
/* b64 is a Base64 string */
|
||||
await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64");
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### `react-native-fs`
|
||||
|
||||
The `ascii` encoding returns binary strings compatible with the `binary` type:
|
||||
|
||||
<details open><summary><b>Reading and Writing snippets</b> (click to show)</summary>
|
||||
|
||||
_Reading Data_
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import { readFile } from "react-native-fs";
|
||||
|
||||
const bstr = await readFile(path, "ascii");
|
||||
/* bstr is a binary string */
|
||||
const workbook = XLSX.read(bstr, {type: "binary"});
|
||||
```
|
||||
|
||||
_Writing Data_
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import { writeFile, DocumentDirectoryPath } from "react-native-fs";
|
||||
|
||||
const bstr = XLSX.write(workbook, {type:'binary', bookType:"xlsx"});
|
||||
/* bstr is a binary string */
|
||||
await writeFile(DocumentDirectoryPath + "/sheetjs.xlsx", bstr, "ascii");
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### `expo-file-system`
|
||||
|
||||
:::caution
|
||||
|
||||
Some Expo APIs return URI that cannot be read with `expo-file-system`. This
|
||||
will manifest as an error:
|
||||
|
||||
> Unsupported scheme for location '...'
|
||||
|
||||
The [`expo-document-picker`](#expo-document-picker) snippet makes a local copy.
|
||||
|
||||
:::
|
||||
|
||||
The `EncodingType.Base64` encoding is compatible with `base64` type.
|
||||
|
||||
<details><summary><b>Reading and Writing snippets</b> (click to show)</summary>
|
||||
|
||||
_Reading Data_
|
||||
|
||||
Calling `FileSystem.readAsStringAsync` with `FileSystem.EncodingType.Base64`
|
||||
encoding returns a promise resolving to a string compatible with `base64` type:
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
|
||||
const workbook = XLSX.read(b64, { type: "base64" });
|
||||
```
|
||||
|
||||
_Writing Data_
|
||||
|
||||
The `FileSystem.EncodingType.Base64` encoding accepts Base64 strings:
|
||||
|
||||
```js
|
||||
import * as XLSX from "xlsx";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
|
||||
/* b64 is a Base64 string */
|
||||
await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 });
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Demo
|
||||
|
||||
:::note
|
||||
|
||||
This demo was tested on an Intel Mac on 2022 August 14 with RN `0.67.2`.
|
||||
|
||||
The iOS simulator runs iOS 15.5 on an iPhone 13.
|
||||
|
||||
The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5.
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
There are many moving parts and pitfalls with React Native apps. It is strongly
|
||||
recommended to follow the official React Native tutorials for iOS and Android
|
||||
before approaching this demo. Details like creating an Android Virtual Device
|
||||
are not covered here.
|
||||
|
||||
:::
|
||||
|
||||
This example tries to separate the library-specific functions.
|
||||
|
||||
0) **Follow the official React Native CLI Guide!**
|
||||
|
||||
Development Environment Guide: <https://reactnative.dev/docs/environment-setup>
|
||||
|
||||
Follow the instructions for iOS and for Android. They will cover installation
|
||||
and system configuration. By the end, you should be able to run the sample app
|
||||
in the Android and the iOS simulators.
|
||||
|
||||
1) Create project:
|
||||
|
||||
```
|
||||
npx react-native init SheetJSRN --version="0.67.2"
|
||||
```
|
||||
|
||||
2) Install shared dependencies:
|
||||
|
||||
```bash
|
||||
cd SheetJSRN
|
||||
curl -LO https://oss.sheetjs.com/assets/img/logo.png
|
||||
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
|
||||
npm i -S react-native-table-component react-native-document-picker
|
||||
```
|
||||
|
||||
Refresh iOS project by running `pod install` from the `ios` subfolder:
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
pod install
|
||||
cd ..
|
||||
```
|
||||
|
||||
3) Download [`index.js`](pathname:///mobile/index.js) and replace:
|
||||
|
||||
```bash
|
||||
curl -LO https://docs.sheetjs.com/mobile/index.js
|
||||
```
|
||||
|
||||
Start the iOS emulator:
|
||||
|
||||
```bash
|
||||
npx react-native run-ios
|
||||
```
|
||||
|
||||
You should see the skeleton app:
|
||||
|
||||
![React Native iOS App](pathname:///mobile/rnios1.png)
|
||||
|
||||
4) Pick a filesystem library for integration:
|
||||
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="RNBU" label="RNBU">
|
||||
|
||||
Install `react-native-blob-util` dependency:
|
||||
|
||||
```bash
|
||||
npm i -S react-native-blob-util
|
||||
```
|
||||
|
||||
Add the highlighted lines to `index.js`:
|
||||
|
||||
```js title="index.js"
|
||||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||||
|
||||
// highlight-start
|
||||
import { read, write } from 'xlsx';
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import { Platform } from 'react-native';
|
||||
import RNFetchBlob from 'react-native-blob-util';
|
||||
|
||||
async function pickAndParse() {
|
||||
/* rn-fetch-blob / react-native-blob-util need a copy */
|
||||
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
|
||||
let path = f.fileCopyUri;
|
||||
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
|
||||
const res = await RNFetchBlob.fs.readFile(path, 'ascii');
|
||||
return read(new Uint8Array(res), {type: 'buffer'});
|
||||
}
|
||||
|
||||
async function writeWorkbook(wb) {
|
||||
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
|
||||
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
|
||||
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
|
||||
return file;
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
const make_width = ws => {
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="RNFA" label="RNFA">
|
||||
|
||||
Install `react-native-file-access` dependency:
|
||||
|
||||
```bash
|
||||
npm i -S react-native-file-access
|
||||
```
|
||||
|
||||
Add the highlighted lines to `index.js`:
|
||||
|
||||
```js title="index.js"
|
||||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||||
|
||||
// highlight-start
|
||||
import { read, write } from 'xlsx';
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import { Dirs, FileSystem } from 'react-native-file-access';
|
||||
|
||||
async function pickAndParse() {
|
||||
/* react-native-file-access does not need a copy */
|
||||
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
|
||||
const res = await FileSystem.readFile(f.uri, "base64");
|
||||
return read(res, {type: 'base64'});
|
||||
}
|
||||
|
||||
async function writeWorkbook(wb) {
|
||||
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
|
||||
const file = Dirs.DocumentDir + "/sheetjsw.xlsx";
|
||||
await FileSystem.writeFile(file, wbout, "base64");
|
||||
return file;
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
const make_width = ws => {
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="RNFB" label="RNFB">
|
||||
|
||||
Install `rn-fetch-blob` dependency:
|
||||
|
||||
```bash
|
||||
npm i -S rn-fetch-blob
|
||||
```
|
||||
|
||||
Add the highlighted lines to `index.js`:
|
||||
|
||||
```js title="index.js"
|
||||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||||
|
||||
// highlight-start
|
||||
import { read, write } from 'xlsx';
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import { Platform } from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
async function pickAndParse() {
|
||||
/* rn-fetch-blob / react-native-blob-util need a copy */
|
||||
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
|
||||
let path = f.fileCopyUri;
|
||||
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
|
||||
const res = await RNFetchBlob.fs.readFile(path, 'ascii');
|
||||
return read(new Uint8Array(res), {type: 'buffer'});
|
||||
}
|
||||
|
||||
async function writeWorkbook(wb) {
|
||||
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
|
||||
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
|
||||
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
|
||||
return file;
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
const make_width = ws => {
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="RNFS" label="RNFS">
|
||||
|
||||
Install `react-native-fs` dependency:
|
||||
|
||||
```bash
|
||||
npm i -S react-native-fs
|
||||
```
|
||||
|
||||
Add the highlighted lines to `index.js`:
|
||||
|
||||
```js title="index.js"
|
||||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||||
|
||||
// highlight-start
|
||||
import { read, write } from 'xlsx';
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs';
|
||||
|
||||
async function pickAndParse() {
|
||||
/* react-native-fs needs a copy */
|
||||
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
|
||||
const bstr = await readFile(f.fileCopyUri, 'ascii');
|
||||
return read(bstr, {type:'binary'});
|
||||
}
|
||||
|
||||
async function writeWorkbook(wb) {
|
||||
const wbout = write(wb, {type:'binary', bookType:"xlsx"});
|
||||
const file = DocumentDirectoryPath + "/sheetjsw.xlsx";
|
||||
await writeFile(file, wbout, 'ascii');
|
||||
return file;
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
const make_width = ws => {
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="EXPO" label="EXPO">
|
||||
|
||||
:::caution
|
||||
|
||||
At the time of testing, the `npx install-expo-modules` step breaks the Android
|
||||
project. The demo works as expected on iOS.
|
||||
|
||||
:::
|
||||
|
||||
Install `expo-file-system` and `expo-document-picker` dependencies:
|
||||
|
||||
```bash
|
||||
npx install-expo-modules
|
||||
npm i -S expo-file-system expo-document-picker
|
||||
```
|
||||
|
||||
Add the highlighted lines to `index.js`:
|
||||
|
||||
```js title="index.js"
|
||||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||||
|
||||
// highlight-start
|
||||
import { read, write } from 'xlsx';
|
||||
import { getDocumentAsync } from 'expo-document-picker';
|
||||
import { documentDirectory, readAsStringAsync, writeAsStringAsync } from 'expo-file-system';
|
||||
|
||||
async function pickAndParse() {
|
||||
const result = await getDocumentAsync({copyToCacheDirectory: true});
|
||||
const path = result.uri;
|
||||
const res = await readAsStringAsync(path, { encoding: "base64" });
|
||||
return read(res, {type: 'base64'});
|
||||
}
|
||||
|
||||
async function writeWorkbook(wb) {
|
||||
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
|
||||
const file = documentDirectory + "sheetjsw.xlsx";
|
||||
await writeAsStringAsync(file, wbout, { encoding: "base64" });
|
||||
return file;
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
const make_width = ws => {
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
5) Refresh the app:
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
pod install
|
||||
cd ..
|
||||
```
|
||||
|
||||
Once refreshed, the development process must be restarted:
|
||||
|
||||
```bash
|
||||
npx react-native run-ios
|
||||
```
|
||||
|
||||
**iOS Testing**
|
||||
|
||||
The app can be tested with the following sequence in the simulator:
|
||||
|
||||
- Download <https://sheetjs.com/pres.numbers>
|
||||
- In the simulator, click the Home icon to return to the home screen
|
||||
- Click on the "Files" icon
|
||||
- Click and drag `pres.numbers` from a Finder window into the simulator.
|
||||
|
||||
![save file iOS](pathname:///mobile/quasar7a.png)
|
||||
|
||||
- Make sure "On My iPhone" is highlighted and select "Save"
|
||||
- Click the Home icon again then select the `SheetJSRN` app
|
||||
- Click "Import data" and select `pres`:
|
||||
|
||||
![pick file iOS](pathname:///mobile/rnios2.png)
|
||||
|
||||
Once selected, the screen should refresh with new contents:
|
||||
|
||||
![read file iOS](pathname:///mobile/rnios3.png)
|
||||
|
||||
- Click "Export data". You will see a popup with a location:
|
||||
|
||||
![write file iOS](pathname:///mobile/rnios4.png)
|
||||
|
||||
- Find the file and verify the contents are correct:
|
||||
|
||||
```bash
|
||||
find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx |
|
||||
while read x; do echo "$x"; npx xlsx-cli "$x"; done
|
||||
```
|
||||
|
||||
Once testing is complete, stop the simulator and the development process.
|
||||
|
||||
**Android Testing**
|
||||
|
||||
There are no Android-specific steps. Emulator can be started with:
|
||||
|
||||
```bash
|
||||
npx react-native run-android
|
||||
```
|
||||
|
||||
![React Native Android App](pathname:///mobile/rnand1.png)
|
||||
|
||||
The app can be tested with the following sequence in the simulator:
|
||||
|
||||
- Download <https://sheetjs.com/pres.numbers>
|
||||
- Click and drag `pres.numbers` from a Finder window into the simulator.
|
||||
- Click "Import data" and select `pres.numbers`:
|
||||
|
||||
![pick file Android](pathname:///mobile/rnand2.png)
|
||||
|
||||
Once selected, the screen should refresh with new contents:
|
||||
|
||||
![read file Android](pathname:///mobile/rnand3.png)
|
||||
|
||||
- Click "Export data". You will see a popup with a location:
|
||||
|
||||
![write file Android](pathname:///mobile/rnand4.png)
|
||||
|
||||
- Pull the file from the simulator and verify the contents:
|
||||
|
||||
```bash
|
||||
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
|
||||
npx xlsx-cli /tmp/sheetjsw.xlsx
|
||||
```
|
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.
|
||||
|
||||
![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>
|
||||
|
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.
|
||||
|
||||
:::
|
||||
|
||||
![Quasar Step 4](pathname:///mobile/quasar4.png)
|
||||
|
||||
|
||||
5) Add the Dialog plugin to `quasar.config.js`:
|
||||
|
||||
```js title="quasar.config.js"
|
||||
// Quasar plugins
|
||||
// highlight-next-line
|
||||
plugins: ['Dialog']
|
||||
```
|
||||
|
||||
6) In the template section of `src/pages/IndexPage.vue`, add a Save button and
|
||||
a Load file picker component at the bottom of the page:
|
||||
|
||||
```html title="src/pages/IndexPage.vue"
|
||||
<!-- highlight-start -->
|
||||
<q-btn-group>
|
||||
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
|
||||
<q-btn label="Save File" @click="saveFile" />
|
||||
</q-btn-group>
|
||||
<!-- highlight-end -->
|
||||
</q-page>
|
||||
</template>
|
||||
```
|
||||
|
||||
This uses two functions that should be added to the component script:
|
||||
|
||||
```ts title="src/pages/IndexPage.vue"
|
||||
const meta = ref<Meta>({
|
||||
totalCount: 1200
|
||||
});
|
||||
// highlight-start
|
||||
function saveFile() {
|
||||
}
|
||||
async function updateFile(v) {
|
||||
}
|
||||
return { todos, meta, saveFile, updateFile };
|
||||
// highlight-end
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
The app should now show two buttons at the bottom:
|
||||
|
||||
![Quasar Step 6](pathname:///mobile/quasar6.png)
|
||||
|
||||
:::caution
|
||||
|
||||
If the app is blank or not refreshing, delete the app and close the simulator,
|
||||
then restart the development process.
|
||||
|
||||
:::
|
||||
|
||||
7) Wire up the `updateFile` function:
|
||||
|
||||
```ts title="src/pages/IndexPage.vue"
|
||||
import { defineComponent, ref } from 'vue';
|
||||
// highlight-start
|
||||
import { read, write, utils } from 'xlsx';
|
||||
import { useQuasar } from 'quasar';
|
||||
// highlight-end
|
||||
|
||||
export default defineComponent({
|
||||
// ...
|
||||
// highlight-start
|
||||
const $q = useQuasar();
|
||||
function dialogerr(e) { $q.dialog({title: "Error!", message: e.message || String(e)}); }
|
||||
// highlight-end
|
||||
function saveFile() {
|
||||
}
|
||||
async function updateFile(v) {
|
||||
// highlight-start
|
||||
try {
|
||||
const files = (v.target as HTMLInputElement).files;
|
||||
if(!files || files.length == 0) return;
|
||||
|
||||
const wb = read(await files[0].arrayBuffer());
|
||||
|
||||
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
|
||||
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
|
||||
} catch(e) { dialogerr(e); }
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
To test that reading works:
|
||||
|
||||
- Download <https://sheetjs.com/pres.numbers>
|
||||
- In the simulator, click the Home icon to return to the home screen
|
||||
- Click on the "Files" icon
|
||||
- Click and drag `pres.numbers` from a Finder window into the simulator.
|
||||
|
||||
![Quasar Step 7 save file](pathname:///mobile/quasar7a.png)
|
||||
|
||||
- Make sure "On My iPhone" is highlighted and select "Save"
|
||||
- Click the Home icon again then select the `SheetJSQuasar` app
|
||||
- Click the "Load" button, then select "Choose File" and select `pres`:
|
||||
|
||||
![Quasar Step 7 load file](pathname:///mobile/quasar7b.png)
|
||||
|
||||
Once selected, the screen should refresh with new contents:
|
||||
|
||||
![Quasar Step 7 new data](pathname:///mobile/quasar7c.png)
|
||||
|
||||
8) Wire up the `saveFile` function:
|
||||
|
||||
```js
|
||||
function saveFile() {
|
||||
// highlight-start
|
||||
/* generate workbook from state */
|
||||
const ws = utils.json_to_sheet(todos.value);
|
||||
const wb = utils.book_new();
|
||||
utils.book_append_sheet(wb, ws, "SheetJSQuasar");
|
||||
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
|
||||
|
||||
/* save to file */
|
||||
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
|
||||
try {
|
||||
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
|
||||
const msg = `File stored at ${$q.cordova.file.documentsDirectory} ${entry.fullPath}`;
|
||||
entry.createWriter(writer => {
|
||||
try {
|
||||
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
|
||||
writer.onwriteend = () => {
|
||||
try {
|
||||
$q.dialog({title: "Success!", message: msg});
|
||||
} catch(e) { dialogerr(e); }
|
||||
};
|
||||
writer.onerror = dialogerr;
|
||||
writer.write(data);
|
||||
} catch(e) { dialogerr(e); }
|
||||
}, dialogerr);
|
||||
}, dialogerr);
|
||||
} catch(e) { dialogerr(e) }
|
||||
}, dialogerr);
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
The page should revert to the old contents.
|
||||
|
||||
To test that writing works:
|
||||
|
||||
- Click "Save File". You will see a popup with a location:
|
||||
|
||||
![Quasar Step 8](pathname:///mobile/quasar8.png)
|
||||
|
||||
- Find the file and verify the contents are correct. Run in a new terminal:
|
||||
|
||||
```bash
|
||||
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
|
||||
while read x; do echo "$x"; npx xlsx-cli "$x"; done
|
||||
```
|
||||
|
||||
Since the contents reverted, you should see
|
||||
|
||||
```
|
||||
SheetJSQuasar
|
||||
id,content
|
||||
1,ct1
|
||||
2,ct2
|
||||
3,ct3
|
||||
4,ct4
|
||||
5,ct5
|
||||
```
|
||||
|
||||
- Use "Load File" to select `pres.numbers` again. Wait for the app to refresh.
|
||||
|
||||
- Click "Save File", then re-run the command:
|
||||
|
||||
```bash
|
||||
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
|
||||
while read x; do echo "$x"; npx xlsx-cli "$x"; done
|
||||
```
|
||||
|
||||
The contents from `pres.numbers` should show up now, with a new header row:
|
||||
|
||||
```
|
||||
SheetJSQuasar
|
||||
id,content
|
||||
42,Bill Clinton
|
||||
43,GeorgeW Bush
|
||||
44,Barack Obama
|
||||
45,Donald Trump
|
||||
46,Joseph Biden
|
||||
```
|
||||
|
||||
</details>
|
||||
|
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