| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | --- | 
					
						
							|  |  |  | title: iOS and Android Apps | 
					
						
							|  |  |  | --- | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | import Tabs from '@theme/Tabs'; | 
					
						
							|  |  |  | import TabItem from '@theme/TabItem'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | Many mobile app frameworks mix JavaScript / CSS / HTML5 concepts with native | 
					
						
							|  |  |  | extensions and libraries to create a hybrid development experience.  Developers | 
					
						
							|  |  |  | well-versed in web technologies can now build actual mobile applications that | 
					
						
							|  |  |  | run on iOS and Android! | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::warning | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | **The ecosystem has broken backwards-compatibility many times!** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | iOS and Android, as well as the underlying JavaScript frameworks, make breaking | 
					
						
							|  |  |  | changes regularly.  The demos were tested against emulators / real devices at | 
					
						
							|  |  |  | some point in time.  A framework or OS change can render the demos inoperable. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Each demo section will mention test dates and platform versions. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The ["JavaScript Engines"](./engines) section includes samples for JavaScript | 
					
						
							|  |  |  | engines used in the mobile app frameworks.  SheetJS libraries have been tested | 
					
						
							|  |  |  | in the relevant engines and should "just work" with some caveats. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | :::caution `readFile` and `writeFile` | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | `XLSX.readFile` and `XLSX.writeFile` do not work in mobile apps!  The demos | 
					
						
							|  |  |  | include platform-specific details for fetching file data for `XLSX.read` and | 
					
						
							|  |  |  | writing file data generated by `XLSX.write`. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Some platforms provide this functionality as part of the standard library. | 
					
						
							|  |  |  | Other platforms, including React Native, do not.  When the platform does not | 
					
						
							|  |  |  | provide, usually there are third-party modules to provide needed functionality. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | MacOS is required for the iOS demos.  The Android demos were tested on MacOS. | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ## 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). | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | | File system Plugin         | File Picker Plugin             |  OS  | Copy | | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | |:---------------------------|:-------------------------------|:----:|:-----| | 
					
						
							|  |  |  | | `react-native-file-access` | `react-native-document-picker` | `AI` |      | | 
					
						
							|  |  |  | | `react-native-blob-util`   | `react-native-document-picker` | `AI` | YES  | | 
					
						
							|  |  |  | | `rn-fetch-blob`            | `react-native-document-picker` | `AI` | YES  | | 
					
						
							|  |  |  | | `react-native-fs`          | `react-native-document-picker` | `AI` | YES  | | 
					
						
							|  |  |  | | `expo-file-system`         | `expo-document-picker`         | ` I` | YES  | | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### RN File Picker
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The following libraries have been tested: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### `react-native-document-picker`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details open><summary><b>Selecting a file</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When a copy is not needed: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const f = await pickSingle({allowMultiSelection: false, mode: "open" }); | 
					
						
							|  |  |  | const path = f.uri; // this path can be read by RN file plugins | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When a copy is needed: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" }); | 
					
						
							|  |  |  | const path = f.fileCopyUri; // this path can be read by RN file plugins | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### `expo-document-picker`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details><summary><b>Selecting a file</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When using `DocumentPicker.getDocumentAsync`, enable `copyToCacheDirectory`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as DocumentPicker from 'expo-document-picker'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const result = await DocumentPicker.getDocumentAsync({ | 
					
						
							|  |  |  |   // highlight-next-line | 
					
						
							|  |  |  |   copyToCacheDirectory: true, | 
					
						
							|  |  |  |   type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | const path = result.uri; | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### RN File Plugins
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The following libraries have been tested: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### `react-native-blob-util` and `rn-fetch-blob`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::note Historical Context | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `react-native-fetch-blob` project was archived in 2019. At the time, there | 
					
						
							|  |  |  | were a number of project forks.  The maintainers blessed the `rn-fetch-blob` | 
					
						
							|  |  |  | fork as the spiritual successor. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `react-native-blob-util` is an active fork of `rn-fetch-blob` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | On the day that this demo was tested (2022 August 14), both `rn-fetch-blob` and | 
					
						
							|  |  |  | `react-native-blob-util` worked with the tested iOS and Android SDK versions. | 
					
						
							|  |  |  | The APIs are identical for the purposes of working with files. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `ascii` type returns an array of numbers corresponding to the raw bytes. | 
					
						
							|  |  |  | A `Uint8Array` from the data is compatible with the `buffer` type. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details open><summary><b>Reading and Writing snippets</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The snippets use `rn-fetch-blob`.  To use `react-native-blob-util`, change the | 
					
						
							|  |  |  | `import` statements to load the module. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Reading Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util | 
					
						
							|  |  |  | const { readFile } = RNFetchBlob.fs; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const res = await readFile(path, 'ascii'); | 
					
						
							|  |  |  | const wb = XLSX.read(new Uint8Array(res), {type:'buffer'}); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | On iOS, the URI from `react-native-document-picker` must be massaged: | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util | 
					
						
							|  |  |  | const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const f = await pickSingle({ | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |   // Instruct the document picker to copy file to Documents directory | 
					
						
							|  |  |  |   copyTo: "documentDirectory", | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |   allowMultiSelection: false, mode: "open" }); | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | // `f.uri` is the original path and `f.fileCopyUri` is the path to the copy | 
					
						
							|  |  |  | let path = f.fileCopyUri; | 
					
						
							|  |  |  | // iOS workaround | 
					
						
							|  |  |  | if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/"); | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const res = await readFile(path, 'ascii'); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Writing Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util | 
					
						
							|  |  |  | const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"}); | 
					
						
							|  |  |  | const file = DocumentDir + "/sheetjsw.xlsx"; | 
					
						
							|  |  |  | const res = await writeFile(file, Array.from(wbout), 'ascii'); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### `react-native-file-access`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `base64` encoding returns strings compatible with the `base64` type: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details open><summary><b>Reading and Writing snippets</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Reading Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import { FileSystem } from "react-native-file-access"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const b64 = await FileSystem.readFile(path, "base64"); | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | /* b64 is a Base64 string */ | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 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"}); | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | /* b64 is a Base64 string */ | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64"); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### `react-native-fs`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `ascii` encoding returns binary strings compatible with the `binary` type: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details open><summary><b>Reading and Writing snippets</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Reading Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import { readFile } from "react-native-fs"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const bstr = await readFile(path, "ascii"); | 
					
						
							|  |  |  | /* bstr is a binary string */ | 
					
						
							|  |  |  | const workbook = XLSX.read(bstr, {type: "binary"}); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Writing Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import { writeFile, DocumentDirectoryPath } from "react-native-fs"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const bstr = XLSX.write(workbook, {type:'binary', bookType:"xlsx"}); | 
					
						
							|  |  |  | /* bstr is a binary string */ | 
					
						
							|  |  |  | await writeFile(DocumentDirectoryPath + "/sheetjs.xlsx", bstr, "ascii"); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### `expo-file-system`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | Some Expo APIs return URI that cannot be read with `expo-file-system`. This | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | will manifest as an error: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | > Unsupported scheme for location '...'
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The [`expo-document-picker`](#expo-document-picker) snippet makes a local copy. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `EncodingType.Base64` encoding is compatible with `base64` type. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details><summary><b>Reading and Writing snippets</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Reading Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Calling `FileSystem.readAsStringAsync` with `FileSystem.EncodingType.Base64` | 
					
						
							|  |  |  | encoding returns a promise resolving to a string compatible with `base64` type: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import * as FileSystem from 'expo-file-system'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 }); | 
					
						
							|  |  |  | const workbook = XLSX.read(b64, { type: "base64" }); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _Writing Data_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `FileSystem.EncodingType.Base64` encoding accepts Base64 strings: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  | import * as XLSX from "xlsx"; | 
					
						
							|  |  |  | import * as FileSystem from 'expo-file-system'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"}); | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | /* b64 is a Base64 string */ | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 }); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::warning | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | There are many moving parts and pitfalls with React Native apps. It is strongly | 
					
						
							|  |  |  | recommended to follow the official React Native tutorials for iOS and Android | 
					
						
							|  |  |  | before approaching this demo.  Details like creating an Android Virtual Device | 
					
						
							|  |  |  | are not covered here. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details open><summary><b>Complete Example</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This example tries to separate the library-specific functions. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | 0) **Follow the official React Native CLI Guide!** | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | Development Environment Guide: <http://reactnative.dev/docs/environment-setup> | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | Follow the instructions for iOS and for Android.  They will cover installation | 
					
						
							|  |  |  | and system configuration.  By the end, you should be able to run the sample app | 
					
						
							|  |  |  | in the Android and the iOS simulators. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Create project: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | npx react-native init SheetJSRN --version="0.67.2" | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Install shared dependencies: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | cd SheetJSRN | 
					
						
							|  |  |  | curl -LO http://oss.sheetjs.com/assets/img/logo.png | 
					
						
							|  |  |  | npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | 
					
						
							|  |  |  | npm i -S react-native-table-component react-native-document-picker | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Refresh iOS project by running `pod install` from the `ios` subfolder: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | cd ios | 
					
						
							|  |  |  | pod install | 
					
						
							|  |  |  | cd .. | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Download [`index.js`](pathname:///mobile/index.js) and replace: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | curl -LO https://docs.sheetjs.com/mobile/index.js | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Start the iOS emulator: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx react-native run-ios | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | You should see the skeleton app: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 4) Pick a filesystem library for integration: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <Tabs> | 
					
						
							|  |  |  |   <TabItem value="RNBU" label="RNBU"> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Install `react-native-blob-util` dependency: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm i -S react-native-blob-util | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Add the highlighted lines to `index.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="index.js" | 
					
						
							|  |  |  | import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | import { Platform } from 'react-native'; | 
					
						
							|  |  |  | import RNFetchBlob from 'react-native-blob-util'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function pickAndParse() { | 
					
						
							|  |  |  |   /* rn-fetch-blob / react-native-blob-util need a copy */ | 
					
						
							|  |  |  |   const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" }); | 
					
						
							|  |  |  |   let path = f.fileCopyUri; | 
					
						
							|  |  |  |   if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/"); | 
					
						
							|  |  |  |   const res = await RNFetchBlob.fs.readFile(path, 'ascii'); | 
					
						
							|  |  |  |   return read(new Uint8Array(res), {type: 'buffer'}); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function writeWorkbook(wb) { | 
					
						
							|  |  |  |   const wbout = write(wb, {type:'buffer', bookType:"xlsx"}); | 
					
						
							|  |  |  |   const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx"; | 
					
						
							|  |  |  |   await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii'); | 
					
						
							|  |  |  |   return file; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const make_width = ws => { | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   </TabItem> | 
					
						
							|  |  |  |   <TabItem value="RNFA" label="RNFA"> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Install `react-native-file-access` dependency: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm i -S react-native-file-access | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Add the highlighted lines to `index.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="index.js" | 
					
						
							|  |  |  | import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | import { Dirs, FileSystem } from 'react-native-file-access'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function pickAndParse() { | 
					
						
							|  |  |  |   /* react-native-file-access does not need a copy */ | 
					
						
							|  |  |  |   const f = await pickSingle({allowMultiSelection: false, mode: "open" }); | 
					
						
							|  |  |  |   const res = await FileSystem.readFile(f.uri, "base64"); | 
					
						
							|  |  |  |   return read(res, {type: 'base64'}); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function writeWorkbook(wb) { | 
					
						
							|  |  |  |   const wbout = write(wb, {type:'base64', bookType:"xlsx"}); | 
					
						
							|  |  |  |   const file = Dirs.DocumentDir + "/sheetjsw.xlsx"; | 
					
						
							|  |  |  |   await FileSystem.writeFile(file, wbout, "base64"); | 
					
						
							|  |  |  |   return file; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const make_width = ws => { | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   </TabItem> | 
					
						
							|  |  |  |   <TabItem value="RNFB" label="RNFB"> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Install `rn-fetch-blob` dependency: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm i -S rn-fetch-blob | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Add the highlighted lines to `index.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="index.js" | 
					
						
							|  |  |  | import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | import { Platform } from 'react-native'; | 
					
						
							|  |  |  | import RNFetchBlob from 'rn-fetch-blob'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function pickAndParse() { | 
					
						
							|  |  |  |   /* rn-fetch-blob / react-native-blob-util need a copy */ | 
					
						
							|  |  |  |   const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" }); | 
					
						
							|  |  |  |   let path = f.fileCopyUri; | 
					
						
							|  |  |  |   if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/"); | 
					
						
							|  |  |  |   const res = await RNFetchBlob.fs.readFile(path, 'ascii'); | 
					
						
							|  |  |  |   return read(new Uint8Array(res), {type: 'buffer'}); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function writeWorkbook(wb) { | 
					
						
							|  |  |  |   const wbout = write(wb, {type:'buffer', bookType:"xlsx"}); | 
					
						
							|  |  |  |   const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx"; | 
					
						
							|  |  |  |   await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii'); | 
					
						
							|  |  |  |   return file; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const make_width = ws => { | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   </TabItem> | 
					
						
							|  |  |  |   <TabItem value="RNFS" label="RNFS"> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Install `react-native-fs` dependency: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm i -S react-native-fs | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Add the highlighted lines to `index.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="index.js" | 
					
						
							|  |  |  | import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { pickSingle } from 'react-native-document-picker'; | 
					
						
							|  |  |  | import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function pickAndParse() { | 
					
						
							|  |  |  |   /* react-native-fs needs a copy */ | 
					
						
							|  |  |  |   const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" }); | 
					
						
							|  |  |  |   const bstr = await readFile(f.fileCopyUri, 'ascii'); | 
					
						
							|  |  |  |   return read(bstr, {type:'binary'}); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function writeWorkbook(wb) { | 
					
						
							|  |  |  |   const wbout = write(wb, {type:'binary', bookType:"xlsx"}); | 
					
						
							|  |  |  |   const file = DocumentDirectoryPath + "/sheetjsw.xlsx"; | 
					
						
							|  |  |  |   await writeFile(file, wbout, 'ascii'); | 
					
						
							|  |  |  |   return file; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const make_width = ws => { | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   </TabItem> | 
					
						
							|  |  |  |   <TabItem value="EXPO" label="EXPO"> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | At the time of testing, the `npx install-expo-modules` step breaks the Android | 
					
						
							|  |  |  | project. The demo works as expected on iOS. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Install `expo-file-system` and `expo-document-picker` dependencies: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx install-expo-modules | 
					
						
							|  |  |  | npm i -S expo-file-system expo-document-picker | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Add the highlighted lines to `index.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="index.js" | 
					
						
							|  |  |  | import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { getDocumentAsync } from 'expo-document-picker'; | 
					
						
							|  |  |  | import { documentDirectory, readAsStringAsync, writeAsStringAsync } from 'expo-file-system'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function pickAndParse() { | 
					
						
							|  |  |  |   const result = await getDocumentAsync({copyToCacheDirectory: true}); | 
					
						
							|  |  |  |   const path = result.uri; | 
					
						
							|  |  |  |   const res = await readAsStringAsync(path, { encoding: "base64" }); | 
					
						
							|  |  |  |   return read(res, {type: 'base64'}); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function writeWorkbook(wb) { | 
					
						
							|  |  |  |   const wbout = write(wb, {type:'base64', bookType:"xlsx"}); | 
					
						
							|  |  |  |   const file = documentDirectory + "sheetjsw.xlsx"; | 
					
						
							|  |  |  |   await writeAsStringAsync(file, wbout, { encoding: "base64" }); | 
					
						
							|  |  |  |   return file; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const make_width = ws => { | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   </TabItem> | 
					
						
							|  |  |  | </Tabs> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 5) Refresh the app: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | cd ios | 
					
						
							|  |  |  | pod install | 
					
						
							|  |  |  | cd .. | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | Once refreshed, the development process must be restarted: | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx react-native run-ios | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | **iOS Testing** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The app can be tested with the following sequence in the simulator: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Download <https://sheetjs.com/pres.numbers> | 
					
						
							|  |  |  | - In the simulator, click the Home icon to return to the home screen | 
					
						
							|  |  |  | - Click on the "Files" icon | 
					
						
							|  |  |  | - Click and drag `pres.numbers` from a Finder window into the simulator. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Make sure "On My iPhone" is highlighted and select "Save" | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | - Click the Home icon again then select the `SheetJSRN` app | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | - Click "Import data" and select `pres`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Once selected, the screen should refresh with new contents: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Click "Export data".  You will see a popup with a location: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Find the file and verify the contents are correct: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx | | 
					
						
							|  |  |  |   while read x; do echo "$x"; npx xlsx-cli "$x"; done | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | Once testing is complete, stop the simulator and the development process. | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | **Android Testing** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | There are no Android-specific steps.  Emulator can be started with: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx react-native run-android | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The app can be tested with the following sequence in the simulator: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Download <https://sheetjs.com/pres.numbers> | 
					
						
							|  |  |  | - Click and drag `pres.numbers` from a Finder window into the simulator. | 
					
						
							|  |  |  | - Click "Import data" and select `pres.numbers`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Once selected, the screen should refresh with new contents: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Click "Export data".  You will see a popup with a location: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Pull the file from the simulator and verify the contents: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx | 
					
						
							|  |  |  | npx xlsx-cli /tmp/sheetjsw.xlsx | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | ## NativeScript
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::note | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-13 22:01:26 +00:00
										 |  |  | This demo was tested on an Intel Mac on 2022 August 10.  NativeScript version | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | (as verified with `ns --version`) is `8.3.2`.  The iOS simulator runs iOS 15.5 | 
					
						
							|  |  |  | on an iPhone SE 3rd generation. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::warning Binary Data issues | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | NativeScript will not safely transmit binary or UTF-8 strings. XLSB, NUMBERS, | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [This is a known NativeScript bug](https://github.com/NativeScript/NativeScript/issues/9586) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This demo will focus on ASCII CSV files.  Once the bug is resolved, XLSX and | 
					
						
							|  |  |  | other formats will be supported. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `@nativescript/core/file-system` package provides classes for file access. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Integration Details
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Reading and writing data require a file handle.  The following snippet searches | 
					
						
							|  |  |  | typical document folders for a specified filename: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts | 
					
						
							|  |  |  | import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function get_handle_for_filename(filename: string): File { | 
					
						
							|  |  |  |   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | 
					
						
							|  |  |  |   const url: string = path.normalize(target.path + "///" + filename); | 
					
						
							|  |  |  |   return File.fromPath(url); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The encoding `ISO_8859_1` spiritually resembles the `"binary"` SheetJS type | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | **Reading data** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `File#readText(encoding.ISO_8859_1)` returns strings compatible with `"binary"` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts | 
					
						
							|  |  |  | /* get binary string */ | 
					
						
							|  |  |  | const bstr: string = await file.readText(encoding.ISO_8859_1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* read workbook */ | 
					
						
							|  |  |  | const wb = read(bstr, { type: "binary" }); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | **Writing data** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `File#writeText` with the `ISO_8859_1` encoding accepts `"binary"` strings with | 
					
						
							|  |  |  | the caveat listed in the warning at the top of this section: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts | 
					
						
							|  |  |  | /* generate binary string */ | 
					
						
							|  |  |  | const bstr: string = write(wb, { bookType: 'csv', type: 'binary' }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* attempt to save binary string to file */ | 
					
						
							|  |  |  | await file.writeText(bstr, encoding.ISO_8859_1); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The demo builds off of the NativeScript + Angular example.  Familiarity with | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | Angular and TypeScript is assumed. | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-13 22:01:26 +00:00
										 |  |  | <details><summary><b>Complete Example</b> (click to show)</summary> | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | 0) Follow the official Environment Setup instructions (tested with "MacOS + iOS") | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 1) Create a skeleton NativeScript + Angular app: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | ns create SheetJSNS --ng | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Launch the app in the iOS simulator to verify that the demo built properly: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | cd SheetJSNS | 
					
						
							|  |  |  | ns run ios | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | (this may take a while) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Once the simulator launches and the test app is displayed, end the script by | 
					
						
							|  |  |  | selecting the terminal and entering the key sequence `CTRL + C` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) From the project folder, install the library: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | 
					
						
							| 
									
										
										
										
											2022-08-10 22:11:05 +00:00
										 |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 4) To confirm the library was loaded, change the title to show the version.  The | 
					
						
							|  |  |  | differences are highlighted. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `src/app/item/items.component.ts` imports the version string to the component: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts title="src/app/item/items.component.ts" | 
					
						
							|  |  |  | // highlight-next-line | 
					
						
							|  |  |  | import { version } from 'xlsx'; | 
					
						
							|  |  |  | import { Component, OnInit } from '@angular/core' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { Item } from './item' | 
					
						
							|  |  |  | import { ItemService } from './item.service' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @Component({ | 
					
						
							|  |  |  |   selector: 'ns-items', | 
					
						
							|  |  |  |   templateUrl: './items.component.html', | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | export class ItemsComponent implements OnInit { | 
					
						
							|  |  |  |   items: Array<Item> | 
					
						
							|  |  |  |   // highlight-next-line | 
					
						
							|  |  |  |   version = `SheetJS - ${version}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(private itemService: ItemService) {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   ngOnInit(): void { | 
					
						
							|  |  |  |     this.items = this.itemService.getItems() | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `src/app/item/items.component.html` references the version in the title: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```xml title="src/app/item/items.component.html" | 
					
						
							|  |  |  | <!-- highlight-next-line --> | 
					
						
							|  |  |  | <ActionBar [title]="version"></ActionBar> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <GridLayout> | 
					
						
							|  |  |  |   <ListView [items]="items"> | 
					
						
							|  |  |  |     <ng-template let-item="item"> | 
					
						
							|  |  |  |       <StackLayout [nsRouterLink]="['/item', item.id]"> | 
					
						
							|  |  |  |         <Label [text]="item.name"></Label> | 
					
						
							|  |  |  |       </StackLayout> | 
					
						
							|  |  |  |     </ng-template> | 
					
						
							|  |  |  |   </ListView> | 
					
						
							|  |  |  | </GridLayout> | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Relaunch the app with `ns run ios` and the title bar should show the version. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 5) Add the Import and Export buttons to the template: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```xml title="src/app/item/items.component.html" | 
					
						
							|  |  |  | <ActionBar [title]="version"></ActionBar> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <!-- highlight-start --> | 
					
						
							|  |  |  | <StackLayout> | 
					
						
							|  |  |  |   <StackLayout orientation="horizontal"> | 
					
						
							|  |  |  |     <Button text="Import File" (tap)="import()" style="padding: 10px"></Button> | 
					
						
							|  |  |  |     <Button text="Export File" (tap)="export()" style="padding: 10px"></Button> | 
					
						
							|  |  |  |   </StackLayout> | 
					
						
							|  |  |  | <!-- highlight-end --> | 
					
						
							|  |  |  |   <ListView [items]="items"> | 
					
						
							|  |  |  |     <ng-template let-item="item"> | 
					
						
							|  |  |  |       <StackLayout [nsRouterLink]="['/item', item.id]"> | 
					
						
							|  |  |  |         <Label [text]="item.name"></Label> | 
					
						
							|  |  |  |       </StackLayout> | 
					
						
							|  |  |  |     </ng-template> | 
					
						
							|  |  |  |   </ListView> | 
					
						
							|  |  |  | <!-- highlight-next-line --> | 
					
						
							|  |  |  | </StackLayout> | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts title="src/app/item/items.component.ts" | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { version, utils, read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { Dialogs } from '@nativescript/core'; | 
					
						
							|  |  |  | import { encoding } from '@nativescript/core/text'; | 
					
						
							|  |  |  | import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | import { Component, OnInit } from '@angular/core' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { Item } from './item' | 
					
						
							|  |  |  | import { ItemService } from './item.service' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | function get_handle_for_filename(filename: string): [File, string] { | 
					
						
							|  |  |  |   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | 
					
						
							|  |  |  |   const url: string = path.normalize(target.path + "///" + filename); | 
					
						
							|  |  |  |   return [File.fromPath(url), url]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @Component({ | 
					
						
							|  |  |  |   selector: 'ns-items', | 
					
						
							|  |  |  |   templateUrl: './items.component.html', | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | export class ItemsComponent implements OnInit { | 
					
						
							|  |  |  |   items: Array<Item> | 
					
						
							|  |  |  |   version: string = `SheetJS - ${version}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(private itemService: ItemService) {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   ngOnInit(): void { | 
					
						
							|  |  |  |     this.items = this.itemService.getItems() | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // highlight-start | 
					
						
							|  |  |  |   /* Import button */ | 
					
						
							|  |  |  |   async import() { | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* Export button */ | 
					
						
							|  |  |  |   async export() { | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   // highlight-end | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Restart the app process and two buttons should show up at the top: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 6) Implement import and export: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts title="src/app/item/items.component.ts" | 
					
						
							|  |  |  | import { version, utils, read, write } from 'xlsx'; | 
					
						
							|  |  |  | import { Dialogs } from '@nativescript/core'; | 
					
						
							|  |  |  | import { encoding } from '@nativescript/core/text'; | 
					
						
							|  |  |  | import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | 
					
						
							|  |  |  | import { Component, OnInit } from '@angular/core' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { Item } from './item' | 
					
						
							|  |  |  | import { ItemService } from './item.service' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function get_handle_for_filename(filename: string): [File, string] { | 
					
						
							|  |  |  |   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | 
					
						
							|  |  |  |   const url: string = path.normalize(target.path + "///" + filename); | 
					
						
							|  |  |  |   return [File.fromPath(url), url]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @Component({ | 
					
						
							|  |  |  |   selector: 'ns-items', | 
					
						
							|  |  |  |   templateUrl: './items.component.html', | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | export class ItemsComponent implements OnInit { | 
					
						
							|  |  |  |   items: Array<Item> | 
					
						
							|  |  |  |   version: string = `SheetJS - ${version}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(private itemService: ItemService) {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   ngOnInit(): void { | 
					
						
							|  |  |  |     this.items = this.itemService.getItems() | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* Import button */ | 
					
						
							|  |  |  |   async import() { | 
					
						
							|  |  |  |     // highlight-start | 
					
						
							|  |  |  |     /* find appropriate path */ | 
					
						
							|  |  |  |     const [file, url] = get_handle_for_filename("SheetJSNS.csv"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       /* get binary string */ | 
					
						
							|  |  |  |       const bstr: string = await file.readText(encoding.ISO_8859_1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* read workbook */ | 
					
						
							|  |  |  |       const wb = read(bstr, { type: "binary" }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* grab first sheet */ | 
					
						
							|  |  |  |       const wsname: string = wb.SheetNames[0]; | 
					
						
							|  |  |  |       const ws = wb.Sheets[wsname]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* update table */ | 
					
						
							|  |  |  |       this.items = utils.sheet_to_json<Item>(ws); | 
					
						
							|  |  |  |       Dialogs.alert(`Attempting to read to ${filename} in ${url}`); | 
					
						
							|  |  |  |     } catch(e) { Dialogs.alert(e.message); } | 
					
						
							|  |  |  |     // highlight-end | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* Export button */ | 
					
						
							|  |  |  |   async export() { | 
					
						
							|  |  |  |     // highlight-start | 
					
						
							|  |  |  |     /* find appropriate path */ | 
					
						
							|  |  |  |     const [file, url] = get_handle_for_filename("SheetJSNS.csv"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       /* create worksheet from data */ | 
					
						
							|  |  |  |       const ws = utils.json_to_sheet(this.items); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* create workbook from worksheet */ | 
					
						
							|  |  |  |       const wb = utils.book_new(); | 
					
						
							|  |  |  |       utils.book_append_sheet(wb, ws, "Sheet1"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* generate binary string */ | 
					
						
							|  |  |  |       const wbout: string = write(wb, { bookType: 'csv', type: 'binary' }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* attempt to save binary string to file */ | 
					
						
							|  |  |  |       await file.writeText(wbout, encoding.ISO_8859_1); | 
					
						
							|  |  |  |       Dialogs.alert(`Wrote to ${filename} in ${url}`); | 
					
						
							|  |  |  |     } catch(e) { Dialogs.alert(e.message); } | 
					
						
							|  |  |  |     // highlight-end | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Restart the app process. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | **Testing** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The app can be tested with the following sequence in the simulator: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Hit "Export File".  A dialog will print where the file was written | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Open that file with a text editor.  It will be a 3-column CSV: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```csv | 
					
						
							|  |  |  | id,name,role | 
					
						
							|  |  |  | 1,Ter Stegen,Goalkeeper | 
					
						
							|  |  |  | 3,Piqué,Defender | 
					
						
							|  |  |  | 4,I. Rakitic,Midfielder | 
					
						
							|  |  |  | ... | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | After the header row, add the line `0,SheetJS,Library`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```csv | 
					
						
							|  |  |  | id,name,role | 
					
						
							|  |  |  | 0,SheetJS,Library | 
					
						
							|  |  |  | 1,Ter Stegen,Goalkeeper | 
					
						
							|  |  |  | 3,Piqué,Defender | 
					
						
							|  |  |  | ... | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Hit "Import File".  A dialog will print the path of the file that was read. | 
					
						
							|  |  |  |   The first item in the list will change: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ## Quasar
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::note | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This demo was tested on an Intel Mac on 2022 August 14. Quasar version `2.7.7`. | 
					
						
							|  |  |  | The iOS simulator runs iOS 15.5 on an iPhone SE 3rd generation. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | This demo will use the Quasar ViteJS starter project with VueJS and Cordova. | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ### Integration Details
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The complete solution uses `cordova-plugin-file` for file operations.  It can | 
					
						
							|  |  |  | be installed like any other Cordova plugin: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | cd src-cordova | 
					
						
							|  |  |  | cordova plugin add cordova-plugin-file | 
					
						
							|  |  |  | cd .. | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Reading data
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `q-file` component presents an API reminiscent of File Input elements: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```html | 
					
						
							|  |  |  | <q-file label="Load File" filled label-color="orange" @input="updateFile"/> | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When binding to the `input` element, the callback receives an `Event` object: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts | 
					
						
							|  |  |  | import { read } from 'xlsx'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // assuming `todos` is a standard VueJS `ref` | 
					
						
							|  |  |  | async function updateFile(v) { try { | 
					
						
							|  |  |  |   // `v.target.files[0]` is the desired file object | 
					
						
							|  |  |  |   const files = (v.target as HTMLInputElement).files; | 
					
						
							|  |  |  |   if(!files || files.length == 0) return; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // read first file | 
					
						
							|  |  |  |   const wb = read(await files[0].arrayBuffer()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // get data of first worksheet as an array of objects | 
					
						
							|  |  |  |   const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // update state | 
					
						
							|  |  |  |   todos.value = data.map(row => ({id: row.Index, content: row.Name})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | } catch(e) { console.log(e); } } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Writing data
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The API is shaped like the File and Directory Entries API.  For clarity, since | 
					
						
							|  |  |  | the code is a "pyramid of doom", the error handlers are omitted: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts | 
					
						
							|  |  |  | import { write } from 'xlsx'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array` | 
					
						
							|  |  |  | const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"}); | 
					
						
							|  |  |  | // Request filesystem access for persistent storage | 
					
						
							|  |  |  | window.requestFileSystem(window.PERSISTENT, 0, function(fs) { | 
					
						
							|  |  |  |   // Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary | 
					
						
							|  |  |  |   fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => { | 
					
						
							|  |  |  |     // Request a FileWriter for writing data | 
					
						
							|  |  |  |     entry.createWriter(writer => { | 
					
						
							|  |  |  |       // The FileWriter API needs an actual Blob | 
					
						
							|  |  |  |       const data = new Blob([u8], {type: "application/vnd.ms-excel"}); | 
					
						
							|  |  |  |       // This callback is called if the write is successful | 
					
						
							|  |  |  |       writer.onwriteend = () => { | 
					
						
							|  |  |  |         // TODO: show a dialog | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       // writer.onerror will be invoked if there is an error in writing | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // write the data | 
					
						
							|  |  |  |       writer.write(data); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | The demo draws from the ViteJS example.  Familiarity with VueJS and TypeScript | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | is assumed. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-15 03:07:34 +00:00
										 |  |  | <details><summary><b>Complete Example</b> (click to show)</summary> | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 0) Ensure all of the dependencies are installed.  Install the CLI globally: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm i -g @quasar/cli cordova | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | (you may need to run `sudo npm i -g` if there are permission issues) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Create a new app: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm init quasar | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | <!-- spellchecker-disable --> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 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: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | <!-- spellchecker-enable --> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | ```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 .. | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | 4) Start the development server: | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | quasar dev -m ios | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | If the app is blank or not refreshing, delete the app and close the simulator, | 
					
						
							|  |  |  | then restart the development process. | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 5) Add the Dialog plugin to `quasar.config.js`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="quasar.config.js" | 
					
						
							|  |  |  |       // Quasar plugins | 
					
						
							|  |  |  |       // highlight-next-line | 
					
						
							|  |  |  |       plugins: ['Dialog'] | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 6) In the template section of `src/pages/IndexPage.vue`, add a Save button and | 
					
						
							|  |  |  |    a Load file picker component at the bottom of the page: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```html title="src/pages/IndexPage.vue" | 
					
						
							|  |  |  |     <!-- highlight-start --> | 
					
						
							|  |  |  |     <q-btn-group> | 
					
						
							|  |  |  |       <q-file label="Load File" filled label-color="orange" @input="updateFile"/> | 
					
						
							|  |  |  |       <q-btn label="Save File" @click="saveFile" /> | 
					
						
							|  |  |  |     </q-btn-group> | 
					
						
							|  |  |  |     <!-- highlight-end --> | 
					
						
							|  |  |  |   </q-page> | 
					
						
							|  |  |  | </template> | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This uses two functions that should be added to the component script: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts title="src/pages/IndexPage.vue" | 
					
						
							|  |  |  |     const meta = ref<Meta>({ | 
					
						
							|  |  |  |       totalCount: 1200 | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |     function saveFile() { | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     async function updateFile(v) { | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return { todos, meta, saveFile, updateFile }; | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The app should now show two buttons at the bottom: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | If the app is blank or not refreshing, delete the app and close the simulator, | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | then restart the development process. | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 7) Wire up the `updateFile` function: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts title="src/pages/IndexPage.vue" | 
					
						
							|  |  |  | import { defineComponent, ref } from 'vue'; | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  | import { read, write, utils } from 'xlsx'; | 
					
						
							|  |  |  | import { useQuasar } from 'quasar'; | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default defineComponent({ | 
					
						
							|  |  |  | // ... | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |     const $q = useQuasar(); | 
					
						
							|  |  |  |     function dialogerr(e) { $q.dialog({title: "Error!", message: e.message || String(e)}); } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |     function saveFile() { | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     async function updateFile(v) { | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         const files = (v.target as HTMLInputElement).files; | 
					
						
							|  |  |  |         if(!files || files.length == 0) return; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const wb = read(await files[0].arrayBuffer()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); | 
					
						
							|  |  |  |         todos.value = data.map(row => ({id: row.Index, content: row.Name})); | 
					
						
							|  |  |  |       } catch(e) { dialogerr(e); } | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | To test that reading works: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Download <https://sheetjs.com/pres.numbers> | 
					
						
							|  |  |  | - In the simulator, click the Home icon to return to the home screen | 
					
						
							|  |  |  | - Click on the "Files" icon | 
					
						
							|  |  |  | - Click and drag `pres.numbers` from a Finder window into the simulator. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Make sure "On My iPhone" is highlighted and select "Save" | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | - Click the Home icon again then select the `SheetJSQuasar` app | 
					
						
							| 
									
										
										
										
											2022-08-14 08:10:18 +00:00
										 |  |  | - Click the "Load" button, then select "Choose File" and select `pres`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Once selected, the screen should refresh with new contents: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 8) Wire up the `saveFile` function: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js | 
					
						
							|  |  |  |     function saveFile() { | 
					
						
							|  |  |  | // highlight-start | 
					
						
							|  |  |  |       /* generate workbook from state */ | 
					
						
							|  |  |  |       const ws = utils.json_to_sheet(todos.value); | 
					
						
							|  |  |  |       const wb = utils.book_new(); | 
					
						
							|  |  |  |       utils.book_append_sheet(wb, ws, "SheetJSQuasar"); | 
					
						
							|  |  |  |       const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* save to file */ | 
					
						
							|  |  |  |       window.requestFileSystem(window.PERSISTENT, 0, function(fs) { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |           fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => { | 
					
						
							|  |  |  |             const msg = `File stored at ${$q.cordova.file.documentsDirectory} ${entry.fullPath}`; | 
					
						
							|  |  |  |             entry.createWriter(writer => { | 
					
						
							|  |  |  |               try { | 
					
						
							|  |  |  |                 const data = new Blob([u8], {type: "application/vnd.ms-excel"}); | 
					
						
							|  |  |  |                 writer.onwriteend = () => { | 
					
						
							|  |  |  |                   try { | 
					
						
							|  |  |  |                     $q.dialog({title: "Success!", message: msg}); | 
					
						
							|  |  |  |                   } catch(e) { dialogerr(e); } | 
					
						
							|  |  |  |                 }; | 
					
						
							|  |  |  |                 writer.onerror = dialogerr; | 
					
						
							|  |  |  |                 writer.write(data); | 
					
						
							|  |  |  |               } catch(e) { dialogerr(e); } | 
					
						
							|  |  |  |             }, dialogerr); | 
					
						
							|  |  |  |           }, dialogerr); | 
					
						
							|  |  |  |         } catch(e) { dialogerr(e) } | 
					
						
							|  |  |  |       }, dialogerr); | 
					
						
							|  |  |  | // highlight-end | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The page should revert to the old contents. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | To test that writing works: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Click "Save File".  You will see a popup with a location: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Find the file and verify the contents are correct.  Run in a new terminal: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx | | 
					
						
							|  |  |  |   while read x; do echo "$x"; npx xlsx-cli "$x"; done | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Since the contents reverted, you should see | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | SheetJSQuasar | 
					
						
							|  |  |  | id,content | 
					
						
							|  |  |  | 1,ct1 | 
					
						
							|  |  |  | 2,ct2 | 
					
						
							|  |  |  | 3,ct3 | 
					
						
							|  |  |  | 4,ct4 | 
					
						
							|  |  |  | 5,ct5 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Use "Load File" to select `pres.numbers` again.  Wait for the app to refresh. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Click "Save File", then re-run the command: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx | | 
					
						
							|  |  |  |   while read x; do echo "$x"; npx xlsx-cli "$x"; done | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The contents from `pres.numbers` should show up now, with a new header row: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | SheetJSQuasar | 
					
						
							|  |  |  | id,content | 
					
						
							|  |  |  | 42,Bill Clinton | 
					
						
							|  |  |  | 43,GeorgeW Bush | 
					
						
							|  |  |  | 44,Barack Obama | 
					
						
							|  |  |  | 45,Donald Trump | 
					
						
							|  |  |  | 46,Joseph Biden | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							| 
									
										
										
										
											2022-08-19 02:26:17 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ## Ionic
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-19 06:42:18 +00:00
										 |  |  | :::note | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 08:22:28 +00:00
										 |  |  | This demo was tested on an Intel Mac on 2022 August 18 with Cordova. | 
					
						
							| 
									
										
										
										
											2022-08-19 06:42:18 +00:00
										 |  |  | The file integration uses `@ionic-native/file` version `5.36.0`. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The iOS simulator runs iOS 15.5 on an iPod Touch 7th Gen. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-19 02:26:17 +00:00
										 |  |  | :::warning Telemetry | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-23 03:20:02 +00:00
										 |  |  | Before starting this demo, manually disable telemetry.  On Linux and MacOS: | 
					
						
							| 
									
										
										
										
											2022-08-19 02:26:17 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | rm -rf ~/.ionic/ | 
					
						
							|  |  |  | mkdir ~/.ionic | 
					
						
							|  |  |  | cat <<EOF > ~/.ionic/config.json | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   "version": "6.20.1", | 
					
						
							|  |  |  |   "telemetry": false, | 
					
						
							|  |  |  |   "npmClient": "npm" | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | EOF | 
					
						
							|  |  |  | npx @capacitor/cli telemetry off | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | To verify telemetry was disabled: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx @ionic/cli config get -g telemetry | 
					
						
							|  |  |  | npx @capacitor/cli telemetry | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Cordova
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-19 06:42:18 +00:00
										 |  |  | The latest version of Ionic uses CapacitorJS. These notes are for Cordova apps. | 
					
						
							| 
									
										
										
										
											2022-08-19 02:26:17 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `Array<Array<any>>` neatly maps to a table with `ngFor`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```html | 
					
						
							|  |  |  | <ion-grid> | 
					
						
							|  |  |  |   <ion-row *ngFor="let row of data"> | 
					
						
							|  |  |  |     <ion-col *ngFor="let val of row"> | 
					
						
							|  |  |  |       {{val}} | 
					
						
							|  |  |  |     </ion-col> | 
					
						
							|  |  |  |   </ion-row> | 
					
						
							|  |  |  | </ion-grid> | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | `@ionic-native/file` reads and writes files on devices. `readAsArrayBuffer` | 
					
						
							|  |  |  | returns `ArrayBuffer` objects suitable for `array` type, and `array` type can | 
					
						
							|  |  |  | be converted to blobs that can be exported with `writeFile`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts | 
					
						
							|  |  |  | /* read a workbook */ | 
					
						
							|  |  |  | const ab: ArrayBuffer = await this.file.readAsArrayBuffer(url, filename); | 
					
						
							|  |  |  | const wb: XLSX.WorkBook = XLSX.read(bstr, {type: 'array'}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* write a workbook */ | 
					
						
							|  |  |  | const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); | 
					
						
							|  |  |  | let blob = new Blob([wbout], {type: 'application/octet-stream'}); | 
					
						
							|  |  |  | this.file.writeFile(url, filename, blob, {replace: true}); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-19 06:42:18 +00:00
										 |  |  | ### Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The demo uses Cordova. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details><summary><b>Complete Example</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 0) Disable telemetry as noted in the warning. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Install required global dependencies: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm i -g cordova-res @angular/cli native-run | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Follow the [React Native demo](#demo) to ensure iOS and Android sims are ready. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 1) Create a new project: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx @ionic/cli start SheetJSIonic blank --type angular --cordova --quiet --no-git --no-link --confirm | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | If a prompt discusses Cordova and Capacitor, enter `Yes` to continue. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | If a prompt asks about creating an Ionic account, enter `N` to opt out. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 2) Set up Cordova: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx @ionic/cli cordova platform add ios --confirm | 
					
						
							|  |  |  | npx @ionic/cli cordova plugin add cordova-plugin-file | 
					
						
							|  |  |  | npm install --save @ionic-native/core @ionic-native/file @ionic/cordova-builders | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 3) Install dependencies: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 4) Add `@ionic-native/file` to the module.  Differences highlighted below: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```ts title="src/app/app.module.ts" | 
					
						
							|  |  |  | import { AppComponent } from './app.component'; | 
					
						
							|  |  |  | import { AppRoutingModule } from './app-routing.module'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // highlight-next-line | 
					
						
							|  |  |  | import { File } from '@ionic-native/file/ngx'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @NgModule({ | 
					
						
							|  |  |  |   declarations: [AppComponent], | 
					
						
							|  |  |  |   imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // highlight-next-line | 
					
						
							|  |  |  |   providers: [File, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], | 
					
						
							|  |  |  |   bootstrap: [AppComponent], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | export class AppModule {} | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 5) Download [`home.page.ts`](pathname:///ionic/home.page.ts) and replace: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | curl -o src/app/home/home.page.ts -L https://docs.sheetjs.com/ionic/home.page.ts | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 6) Test the app: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```bash | 
					
						
							|  |  |  | npx @ionic/cli cordova emulate ios | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> |