diff --git a/docz/docs/03-demos/02-mobile.md b/docz/docs/03-demos/02-mobile.md deleted file mode 100644 index 4e41096..0000000 --- a/docz/docs/03-demos/02-mobile.md +++ /dev/null @@ -1,1704 +0,0 @@ ---- -title: iOS and Android Apps ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -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"](/docs/demos/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. - -::: - -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). - -| 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` - -
Selecting a file (click to show) - -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 -``` - -
- -#### `expo-document-picker` - -
Selecting a file (click to show) - -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; -``` - -
- - -### 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. - -
Reading and Writing snippets (click to show) - -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'); -``` - - -
- -#### `react-native-file-access` - -The `base64` encoding returns strings compatible with the `base64` type: - -
Reading and Writing snippets (click to show) - -_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"); -``` - -
- -#### `react-native-fs` - -The `ascii` encoding returns binary strings compatible with the `binary` type: - -
Reading and Writing snippets (click to show) - -_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"); -``` - -
- -#### `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. - -
Reading and Writing snippets (click to show) - -_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 }); -``` - -
- -### 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. - -::: - -
Complete Example (click to show) - -This example tries to separate the library-specific functions. - -0) **Follow the official React Native CLI Guide!** - -Development Environment Guide: - -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: - - - - - -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 => { -``` - - - - -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 => { -``` - - - - -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 => { -``` - - - - -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 => { -``` - - - - -:::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 => { -``` - - - - - -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 -- 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 -- 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 -``` - -
- -## NativeScript - -:::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. - -::: - -:::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. - -::: - -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 -Angular and TypeScript is assumed. - -
Complete Example (click to show) - -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 - // 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" - - - - - - - - - - - - -``` - -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" - - - - - - - - - - - - - - - - - - -``` - -```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 - 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 - 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(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) - -
- -## 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 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 - -``` - -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 draws from the ViteJS example. Familiarity with VueJS and TypeScript -is assumed. - -
Complete Example (click to show) - -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 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" - - - - - - - - -``` - -This uses two functions that should be added to the component script: - -```ts title="src/pages/IndexPage.vue" - const meta = ref({ - 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 -- 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 -``` - -
- -## Ionic - -:::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. - -::: - -:::warning Telemetry - -Before starting this demo, manually disable telemetry. On Linux and MacOS: - -```bash -rm -rf ~/.ionic/ -mkdir ~/.ionic -cat < ~/.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>` neatly maps to a table with `ngFor`: - -```html - - - - {{val}} - - - -``` - -`@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 - -The demo uses Cordova. - -
Complete Example (click to show) - -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 -``` - -
- -## 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 - - -
- - -
{@html html}
-
-``` - -#### Writing data - -`@capacitor/filesystem` can write Base64 strings: - -```html - - -
- -
{@html html}
-
-``` - -### Demo - -
Complete Example (click to show) - -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" - - -
-

SheetJS × CapacitorJS { version }

- - -
{@html html}
-
-``` - -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. - -
diff --git a/docz/docs/03-demos/02-mobile/01-reactnative.md b/docz/docs/03-demos/02-mobile/01-reactnative.md new file mode 100644 index 0000000..88498bd --- /dev/null +++ b/docz/docs/03-demos/02-mobile/01-reactnative.md @@ -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: + + + + +
iOSAndroid
+ +![iOS screenshot](pathname:///mobile/rnios3.png) + + + +![Android screenshot](pathname:///mobile/rnand3.png) + +
+ + +## 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` + +
Selecting a file (click to show) + +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 +``` + +
+ +#### `expo-document-picker` + +
Selecting a file (click to show) + +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; +``` + +
+ + +### 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. + +
Reading and Writing snippets (click to show) + +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'); +``` + + +
+ +#### `react-native-file-access` + +The `base64` encoding returns strings compatible with the `base64` type: + +
Reading and Writing snippets (click to show) + +_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"); +``` + +
+ +#### `react-native-fs` + +The `ascii` encoding returns binary strings compatible with the `binary` type: + +
Reading and Writing snippets (click to show) + +_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"); +``` + +
+ +#### `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. + +
Reading and Writing snippets (click to show) + +_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 }); +``` + +
+ +## 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: + +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: + + + + + +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 => { +``` + + + + +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 => { +``` + + + + +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 => { +``` + + + + +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 => { +``` + + + + +:::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 => { +``` + + + + + +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 +- 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 +- 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 +``` diff --git a/docz/docs/03-demos/02-mobile/02-nativescript.md b/docz/docs/03-demos/02-mobile/02-nativescript.md new file mode 100644 index 0000000..64c4dd9 --- /dev/null +++ b/docz/docs/03-demos/02-mobile/02-nativescript.md @@ -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. + +::: + +
Complete Example (click to show) + +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 + // 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" + + + + + + + + + + + + +``` + +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" + + + + + + + + + + + + + + + + + + +``` + +```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 + 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 + 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(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) + +
+ diff --git a/docz/docs/03-demos/02-mobile/03-quasar.md b/docz/docs/03-demos/02-mobile/03-quasar.md new file mode 100644 index 0000000..f9ef1fb --- /dev/null +++ b/docz/docs/03-demos/02-mobile/03-quasar.md @@ -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 + +``` + +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. + +
Complete Example (click to show) + +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 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" + + + + + + + + +``` + +This uses two functions that should be added to the component script: + +```ts title="src/pages/IndexPage.vue" + const meta = ref({ + 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 +- 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 +``` + +
+ diff --git a/docz/docs/03-demos/02-mobile/04-ionic.md b/docz/docs/03-demos/02-mobile/04-ionic.md new file mode 100644 index 0000000..e6e879d --- /dev/null +++ b/docz/docs/03-demos/02-mobile/04-ionic.md @@ -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 < ~/.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>` neatly maps to a table with `ngFor`: + +```html + + + + {{val}} + + + +``` + +`@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. + +::: + +
Complete Example (click to show) + +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 +``` + +
+ diff --git a/docz/docs/03-demos/02-mobile/05-capacitor.md b/docz/docs/03-demos/02-mobile/05-capacitor.md new file mode 100644 index 0000000..fd7a82c --- /dev/null +++ b/docz/docs/03-demos/02-mobile/05-capacitor.md @@ -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 + + +
+ + +
{@html html}
+
+``` + +#### Writing data + +`@capacitor/filesystem` can write Base64 strings: + +```html + + +
+ +
{@html html}
+
+``` + +### Demo + +
Complete Example (click to show) + +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" + + +
+

SheetJS × CapacitorJS { version }

+ + +
{@html html}
+
+``` + +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. + +
diff --git a/docz/docs/03-demos/02-mobile/_category_.json b/docz/docs/03-demos/02-mobile/_category_.json new file mode 100644 index 0000000..4fbae02 --- /dev/null +++ b/docz/docs/03-demos/02-mobile/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "iOS and Android Apps", + "position": 3 +} \ No newline at end of file diff --git a/docz/docs/03-demos/02-mobile/index.md b/docz/docs/03-demos/02-mobile/index.md new file mode 100644 index 0000000..cef0475 --- /dev/null +++ b/docz/docs/03-demos/02-mobile/index.md @@ -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. + +
    {useCurrentSidebarCategory().items.map((item, index) => { + const listyle = (item.customProps?.icon) ? { + listStyleImage: `url("${item.customProps.icon}")` + } : {}; + return (
  • + {item.label}{item.customProps?.summary && (" - " + item.customProps.summary)} +
  • ); +})}
+ +:::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. + +::: diff --git a/docz/docs/03-demos/03-desktop/01-electron.md b/docz/docs/03-demos/03-desktop/01-electron.md index fd74a51..a6c1a7d 100644 --- a/docz/docs/03-demos/03-desktop/01-electron.md +++ b/docz/docs/03-demos/03-desktop/01-electron.md @@ -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: diff --git a/docz/docs/03-demos/03-desktop/02-nwjs.md b/docz/docs/03-demos/03-desktop/02-nwjs.md index ff143ae..5b1fa5b 100644 --- a/docz/docs/03-demos/03-desktop/02-nwjs.md +++ b/docz/docs/03-demos/03-desktop/02-nwjs.md @@ -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: diff --git a/docz/docs/03-demos/03-desktop/03-wails.md b/docz/docs/03-demos/03-desktop/03-wails.md index c15dd98..b6db890 100644 --- a/docz/docs/03-demos/03-desktop/03-wails.md +++ b/docz/docs/03-demos/03-desktop/03-wails.md @@ -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: diff --git a/docz/docs/03-demos/03-desktop/04-tauri.md b/docz/docs/03-demos/03-desktop/04-tauri.md index 4abbedb..59524b9 100644 --- a/docz/docs/03-demos/03-desktop/04-tauri.md +++ b/docz/docs/03-demos/03-desktop/04-tauri.md @@ -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: diff --git a/docz/docs/03-demos/03-desktop/05-neutralino.md b/docz/docs/03-demos/03-desktop/05-neutralino.md index 9fc8fd1..4f9568c 100644 --- a/docz/docs/03-demos/03-desktop/05-neutralino.md +++ b/docz/docs/03-demos/03-desktop/05-neutralino.md @@ -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: diff --git a/docz/docs/03-demos/03-desktop/06-reactnative.md b/docz/docs/03-demos/03-desktop/06-reactnative.md index 10c107f..cf76f7c 100644 --- a/docz/docs/03-demos/03-desktop/06-reactnative.md +++ b/docz/docs/03-demos/03-desktop/06-reactnative.md @@ -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) ::: diff --git a/docz/docs/03-demos/03-desktop/index.md b/docz/docs/03-demos/03-desktop/index.md index 54007b1..ec9e6be 100644 --- a/docz/docs/03-demos/03-desktop/index.md +++ b/docz/docs/03-demos/03-desktop/index.md @@ -1,6 +1,6 @@ --- title: Desktop Applications -pagination_prev: demos/mobile +pagination_prev: demos/mobile/index pagination_next: demos/grid --- diff --git a/docz/docs/03-demos/11-angular.md b/docz/docs/03-demos/11-angular.md index c4ee60a..7fd5275 100644 --- a/docz/docs/03-demos/11-angular.md +++ b/docz/docs/03-demos/11-angular.md @@ -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 diff --git a/docz/docs/03-demos/12-react.md b/docz/docs/03-demos/12-react.md index 0293b54..e401ca8 100644 --- a/docz/docs/03-demos/12-react.md +++ b/docz/docs/03-demos/12-react.md @@ -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) diff --git a/docz/docs/03-demos/13-vue.md b/docz/docs/03-demos/13-vue.md index 649deb9..9bc6928 100644 --- a/docz/docs/03-demos/13-vue.md +++ b/docz/docs/03-demos/13-vue.md @@ -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) diff --git a/docz/docs/03-demos/14-svelte.md b/docz/docs/03-demos/14-svelte.md index 155bc8e..2e7c01d 100644 --- a/docz/docs/03-demos/14-svelte.md +++ b/docz/docs/03-demos/14-svelte.md @@ -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 diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index 18139f2..373df48 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -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) diff --git a/docz/static/electron/index.html b/docz/static/electron/index.html index e7c091a..f20cfe4 100644 --- a/docz/static/electron/index.html +++ b/docz/static/electron/index.html @@ -5,6 +5,7 @@ + SheetJS Electron Demo