forked from sheetjs/docs.sheetjs.com
		
	mobile
This commit is contained in:
		
							parent
							
								
									3616b04348
								
							
						
					
					
						commit
						e27218f52a
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										647
									
								
								docz/docs/03-demos/02-mobile/01-reactnative.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										647
									
								
								docz/docs/03-demos/02-mobile/01-reactnative.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,647 @@ | ||||
| --- | ||||
| title: React Native | ||||
| pagination_prev: demos/salesforce | ||||
| pagination_next: demos/desktop/index | ||||
| sidebar_position: 1 | ||||
| sidebar_custom_props: | ||||
|   summary: React + Native Rendering | ||||
| --- | ||||
| 
 | ||||
| import Tabs from '@theme/Tabs'; | ||||
| import TabItem from '@theme/TabItem'; | ||||
| 
 | ||||
| The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported | ||||
| from the main `App.js` entrypoint or any script in the project. | ||||
| 
 | ||||
| The "Complete Example" creates an app that looks like the screenshots below: | ||||
| 
 | ||||
| <table><thead><tr> | ||||
|   <th><a href="#demo">iOS</a></th> | ||||
|   <th><a href="#demo">Android</a></th> | ||||
| </tr></thead><tbody><tr><td> | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| </td><td> | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| </td></tr></tbody></table> | ||||
| 
 | ||||
| 
 | ||||
| ## Native Libraries | ||||
| 
 | ||||
| :::warning | ||||
| 
 | ||||
| React Native does not provide a native file picker or a method for reading and | ||||
| writing data from documents on the devices. A third-party library must be used. | ||||
| 
 | ||||
| Since React Native internals change between releases, libraries may only work | ||||
| with specific versions of React Native.  Project documentation should be | ||||
| consulted before picking a library. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| The following table lists tested file plugins.  "OS" lists tested platforms | ||||
| ("A" for Android and "I" for iOS).  "Copy" indicates whether an explicit copy | ||||
| is needed (file picker copies to cache directory and file plugin reads cache). | ||||
| 
 | ||||
| | File system Plugin         | File Picker Plugin             |  OS  | Copy | | ||||
| |:---------------------------|:-------------------------------|:----:|:-----| | ||||
| | `react-native-file-access` | `react-native-document-picker` | `AI` |      | | ||||
| | `react-native-blob-util`   | `react-native-document-picker` | `AI` | YES  | | ||||
| | `rn-fetch-blob`            | `react-native-document-picker` | `AI` | YES  | | ||||
| | `react-native-fs`          | `react-native-document-picker` | `AI` | YES  | | ||||
| | `expo-file-system`         | `expo-document-picker`         | ` I` | YES  | | ||||
| 
 | ||||
| ### RN File Picker | ||||
| 
 | ||||
| The following libraries have been tested: | ||||
| 
 | ||||
| #### `react-native-document-picker` | ||||
| 
 | ||||
| <details open><summary><b>Selecting a file</b> (click to show)</summary> | ||||
| 
 | ||||
| When a copy is not needed: | ||||
| 
 | ||||
| ```js | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| 
 | ||||
| const f = await pickSingle({allowMultiSelection: false, mode: "open" }); | ||||
| const path = f.uri; // this path can be read by RN file plugins | ||||
| ``` | ||||
| 
 | ||||
| When a copy is needed: | ||||
| 
 | ||||
| ```js | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| 
 | ||||
| const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" }); | ||||
| const path = f.fileCopyUri; // this path can be read by RN file plugins | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| #### `expo-document-picker` | ||||
| 
 | ||||
| <details><summary><b>Selecting a file</b> (click to show)</summary> | ||||
| 
 | ||||
| When using `DocumentPicker.getDocumentAsync`, enable `copyToCacheDirectory`: | ||||
| 
 | ||||
| ```js | ||||
| import * as DocumentPicker from 'expo-document-picker'; | ||||
| 
 | ||||
| const result = await DocumentPicker.getDocumentAsync({ | ||||
|   // highlight-next-line | ||||
|   copyToCacheDirectory: true, | ||||
|   type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] | ||||
| }); | ||||
| const path = result.uri; | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| 
 | ||||
| ### RN File Plugins | ||||
| 
 | ||||
| The following libraries have been tested: | ||||
| 
 | ||||
| #### `react-native-blob-util` and `rn-fetch-blob` | ||||
| 
 | ||||
| :::note Historical Context | ||||
| 
 | ||||
| The `react-native-fetch-blob` project was archived in 2019. At the time, there | ||||
| were a number of project forks.  The maintainers blessed the `rn-fetch-blob` | ||||
| fork as the spiritual successor. | ||||
| 
 | ||||
| `react-native-blob-util` is an active fork of `rn-fetch-blob` | ||||
| 
 | ||||
| On the day that this demo was tested (2022 August 14), both `rn-fetch-blob` and | ||||
| `react-native-blob-util` worked with the tested iOS and Android SDK versions. | ||||
| The APIs are identical for the purposes of working with files. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| The `ascii` type returns an array of numbers corresponding to the raw bytes. | ||||
| A `Uint8Array` from the data is compatible with the `buffer` type. | ||||
| 
 | ||||
| <details open><summary><b>Reading and Writing snippets</b> (click to show)</summary> | ||||
| 
 | ||||
| The snippets use `rn-fetch-blob`.  To use `react-native-blob-util`, change the | ||||
| `import` statements to load the module. | ||||
| 
 | ||||
| _Reading Data_ | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util | ||||
| const { readFile } = RNFetchBlob.fs; | ||||
| 
 | ||||
| const res = await readFile(path, 'ascii'); | ||||
| const wb = XLSX.read(new Uint8Array(res), {type:'buffer'}); | ||||
| ``` | ||||
| 
 | ||||
| :::caution | ||||
| 
 | ||||
| On iOS, the URI from `react-native-document-picker` must be massaged: | ||||
| 
 | ||||
| ```js | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util | ||||
| const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs; | ||||
| 
 | ||||
| const f = await pickSingle({ | ||||
| // highlight-start | ||||
|   // Instruct the document picker to copy file to Documents directory | ||||
|   copyTo: "documentDirectory", | ||||
| // highlight-end | ||||
|   allowMultiSelection: false, mode: "open" }); | ||||
| // highlight-start | ||||
| // `f.uri` is the original path and `f.fileCopyUri` is the path to the copy | ||||
| let path = f.fileCopyUri; | ||||
| // iOS workaround | ||||
| if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/"); | ||||
| // highlight-end | ||||
| 
 | ||||
| const res = await readFile(path, 'ascii'); | ||||
| ``` | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| _Writing Data_ | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util | ||||
| const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs; | ||||
| 
 | ||||
| const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"}); | ||||
| const file = DocumentDir + "/sheetjsw.xlsx"; | ||||
| const res = await writeFile(file, Array.from(wbout), 'ascii'); | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| #### `react-native-file-access` | ||||
| 
 | ||||
| The `base64` encoding returns strings compatible with the `base64` type: | ||||
| 
 | ||||
| <details open><summary><b>Reading and Writing snippets</b> (click to show)</summary> | ||||
| 
 | ||||
| _Reading Data_ | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import { FileSystem } from "react-native-file-access"; | ||||
| 
 | ||||
| const b64 = await FileSystem.readFile(path, "base64"); | ||||
| /* b64 is a Base64 string */ | ||||
| const workbook = XLSX.read(b64, {type: "base64"}); | ||||
| ``` | ||||
| 
 | ||||
| _Writing Data_ | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import { Dirs, FileSystem } from "react-native-file-access"; | ||||
| const DDP = Dirs.DocumentDir + "/"; | ||||
| 
 | ||||
| const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"}); | ||||
| /* b64 is a Base64 string */ | ||||
| await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64"); | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| #### `react-native-fs` | ||||
| 
 | ||||
| The `ascii` encoding returns binary strings compatible with the `binary` type: | ||||
| 
 | ||||
| <details open><summary><b>Reading and Writing snippets</b> (click to show)</summary> | ||||
| 
 | ||||
| _Reading Data_ | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import { readFile } from "react-native-fs"; | ||||
| 
 | ||||
| const bstr = await readFile(path, "ascii"); | ||||
| /* bstr is a binary string */ | ||||
| const workbook = XLSX.read(bstr, {type: "binary"}); | ||||
| ``` | ||||
| 
 | ||||
| _Writing Data_ | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import { writeFile, DocumentDirectoryPath } from "react-native-fs"; | ||||
| 
 | ||||
| const bstr = XLSX.write(workbook, {type:'binary', bookType:"xlsx"}); | ||||
| /* bstr is a binary string */ | ||||
| await writeFile(DocumentDirectoryPath + "/sheetjs.xlsx", bstr, "ascii"); | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| #### `expo-file-system` | ||||
| 
 | ||||
| :::caution | ||||
| 
 | ||||
| Some Expo APIs return URI that cannot be read with `expo-file-system`. This | ||||
| will manifest as an error: | ||||
| 
 | ||||
| > Unsupported scheme for location '...' | ||||
| 
 | ||||
| The [`expo-document-picker`](#expo-document-picker) snippet makes a local copy. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| The `EncodingType.Base64` encoding is compatible with `base64` type. | ||||
| 
 | ||||
| <details><summary><b>Reading and Writing snippets</b> (click to show)</summary> | ||||
| 
 | ||||
| _Reading Data_ | ||||
| 
 | ||||
| Calling `FileSystem.readAsStringAsync` with `FileSystem.EncodingType.Base64` | ||||
| encoding returns a promise resolving to a string compatible with `base64` type: | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import * as FileSystem from 'expo-file-system'; | ||||
| 
 | ||||
| const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 }); | ||||
| const workbook = XLSX.read(b64, { type: "base64" }); | ||||
| ``` | ||||
| 
 | ||||
| _Writing Data_ | ||||
| 
 | ||||
| The `FileSystem.EncodingType.Base64` encoding accepts Base64 strings: | ||||
| 
 | ||||
| ```js | ||||
| import * as XLSX from "xlsx"; | ||||
| import * as FileSystem from 'expo-file-system'; | ||||
| 
 | ||||
| const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"}); | ||||
| /* b64 is a Base64 string */ | ||||
| await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 }); | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| ## Demo | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| This demo was tested on an Intel Mac on 2022 August 14 with RN `0.67.2`. | ||||
| 
 | ||||
| The iOS simulator runs iOS 15.5 on an iPhone 13. | ||||
| 
 | ||||
| The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| :::warning | ||||
| 
 | ||||
| There are many moving parts and pitfalls with React Native apps. It is strongly | ||||
| recommended to follow the official React Native tutorials for iOS and Android | ||||
| before approaching this demo.  Details like creating an Android Virtual Device | ||||
| are not covered here. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| This example tries to separate the library-specific functions. | ||||
| 
 | ||||
| 0) **Follow the official React Native CLI Guide!** | ||||
| 
 | ||||
| Development Environment Guide: <https://reactnative.dev/docs/environment-setup> | ||||
| 
 | ||||
| Follow the instructions for iOS and for Android.  They will cover installation | ||||
| and system configuration.  By the end, you should be able to run the sample app | ||||
| in the Android and the iOS simulators. | ||||
| 
 | ||||
| 1) Create project: | ||||
| 
 | ||||
| ``` | ||||
| npx react-native init SheetJSRN --version="0.67.2" | ||||
| ``` | ||||
| 
 | ||||
| 2) Install shared dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| cd SheetJSRN | ||||
| curl -LO https://oss.sheetjs.com/assets/img/logo.png | ||||
| npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | ||||
| npm i -S react-native-table-component react-native-document-picker | ||||
| ``` | ||||
| 
 | ||||
| Refresh iOS project by running `pod install` from the `ios` subfolder: | ||||
| 
 | ||||
| ```bash | ||||
| cd ios | ||||
| pod install | ||||
| cd .. | ||||
| ``` | ||||
| 
 | ||||
| 3) Download [`index.js`](pathname:///mobile/index.js) and replace: | ||||
| 
 | ||||
| ```bash | ||||
| curl -LO https://docs.sheetjs.com/mobile/index.js | ||||
| ``` | ||||
| 
 | ||||
| Start the iOS emulator: | ||||
| 
 | ||||
| ```bash | ||||
| npx react-native run-ios | ||||
| ``` | ||||
| 
 | ||||
| You should see the skeleton app: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 4) Pick a filesystem library for integration: | ||||
| 
 | ||||
| 
 | ||||
| <Tabs> | ||||
|   <TabItem value="RNBU" label="RNBU"> | ||||
| 
 | ||||
| Install `react-native-blob-util` dependency: | ||||
| 
 | ||||
| ```bash | ||||
| npm i -S react-native-blob-util | ||||
| ``` | ||||
| 
 | ||||
| Add the highlighted lines to `index.js`: | ||||
| 
 | ||||
| ```js title="index.js" | ||||
| import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | ||||
| 
 | ||||
| // highlight-start | ||||
| import { read, write } from 'xlsx'; | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| import { Platform } from 'react-native'; | ||||
| import RNFetchBlob from 'react-native-blob-util'; | ||||
| 
 | ||||
| async function pickAndParse() { | ||||
|   /* rn-fetch-blob / react-native-blob-util need a copy */ | ||||
|   const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" }); | ||||
|   let path = f.fileCopyUri; | ||||
|   if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/"); | ||||
|   const res = await RNFetchBlob.fs.readFile(path, 'ascii'); | ||||
|   return read(new Uint8Array(res), {type: 'buffer'}); | ||||
| } | ||||
| 
 | ||||
| async function writeWorkbook(wb) { | ||||
|   const wbout = write(wb, {type:'buffer', bookType:"xlsx"}); | ||||
|   const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx"; | ||||
|   await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii'); | ||||
|   return file; | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| const make_width = ws => { | ||||
| ``` | ||||
| 
 | ||||
|   </TabItem> | ||||
|   <TabItem value="RNFA" label="RNFA"> | ||||
| 
 | ||||
| Install `react-native-file-access` dependency: | ||||
| 
 | ||||
| ```bash | ||||
| npm i -S react-native-file-access | ||||
| ``` | ||||
| 
 | ||||
| Add the highlighted lines to `index.js`: | ||||
| 
 | ||||
| ```js title="index.js" | ||||
| import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | ||||
| 
 | ||||
| // highlight-start | ||||
| import { read, write } from 'xlsx'; | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| import { Dirs, FileSystem } from 'react-native-file-access'; | ||||
| 
 | ||||
| async function pickAndParse() { | ||||
|   /* react-native-file-access does not need a copy */ | ||||
|   const f = await pickSingle({allowMultiSelection: false, mode: "open" }); | ||||
|   const res = await FileSystem.readFile(f.uri, "base64"); | ||||
|   return read(res, {type: 'base64'}); | ||||
| } | ||||
| 
 | ||||
| async function writeWorkbook(wb) { | ||||
|   const wbout = write(wb, {type:'base64', bookType:"xlsx"}); | ||||
|   const file = Dirs.DocumentDir + "/sheetjsw.xlsx"; | ||||
|   await FileSystem.writeFile(file, wbout, "base64"); | ||||
|   return file; | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| const make_width = ws => { | ||||
| ``` | ||||
| 
 | ||||
|   </TabItem> | ||||
|   <TabItem value="RNFB" label="RNFB"> | ||||
| 
 | ||||
| Install `rn-fetch-blob` dependency: | ||||
| 
 | ||||
| ```bash | ||||
| npm i -S rn-fetch-blob | ||||
| ``` | ||||
| 
 | ||||
| Add the highlighted lines to `index.js`: | ||||
| 
 | ||||
| ```js title="index.js" | ||||
| import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | ||||
| 
 | ||||
| // highlight-start | ||||
| import { read, write } from 'xlsx'; | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| import { Platform } from 'react-native'; | ||||
| import RNFetchBlob from 'rn-fetch-blob'; | ||||
| 
 | ||||
| async function pickAndParse() { | ||||
|   /* rn-fetch-blob / react-native-blob-util need a copy */ | ||||
|   const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" }); | ||||
|   let path = f.fileCopyUri; | ||||
|   if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/"); | ||||
|   const res = await RNFetchBlob.fs.readFile(path, 'ascii'); | ||||
|   return read(new Uint8Array(res), {type: 'buffer'}); | ||||
| } | ||||
| 
 | ||||
| async function writeWorkbook(wb) { | ||||
|   const wbout = write(wb, {type:'buffer', bookType:"xlsx"}); | ||||
|   const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx"; | ||||
|   await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii'); | ||||
|   return file; | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| const make_width = ws => { | ||||
| ``` | ||||
| 
 | ||||
|   </TabItem> | ||||
|   <TabItem value="RNFS" label="RNFS"> | ||||
| 
 | ||||
| Install `react-native-fs` dependency: | ||||
| 
 | ||||
| ```bash | ||||
| npm i -S react-native-fs | ||||
| ``` | ||||
| 
 | ||||
| Add the highlighted lines to `index.js`: | ||||
| 
 | ||||
| ```js title="index.js" | ||||
| import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | ||||
| 
 | ||||
| // highlight-start | ||||
| import { read, write } from 'xlsx'; | ||||
| import { pickSingle } from 'react-native-document-picker'; | ||||
| import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs'; | ||||
| 
 | ||||
| async function pickAndParse() { | ||||
|   /* react-native-fs needs a copy */ | ||||
|   const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" }); | ||||
|   const bstr = await readFile(f.fileCopyUri, 'ascii'); | ||||
|   return read(bstr, {type:'binary'}); | ||||
| } | ||||
| 
 | ||||
| async function writeWorkbook(wb) { | ||||
|   const wbout = write(wb, {type:'binary', bookType:"xlsx"}); | ||||
|   const file = DocumentDirectoryPath + "/sheetjsw.xlsx"; | ||||
|   await writeFile(file, wbout, 'ascii'); | ||||
|   return file; | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| const make_width = ws => { | ||||
| ``` | ||||
| 
 | ||||
|   </TabItem> | ||||
|   <TabItem value="EXPO" label="EXPO"> | ||||
| 
 | ||||
| :::caution | ||||
| 
 | ||||
| At the time of testing, the `npx install-expo-modules` step breaks the Android | ||||
| project. The demo works as expected on iOS. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| Install `expo-file-system` and `expo-document-picker` dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| npx install-expo-modules | ||||
| npm i -S expo-file-system expo-document-picker | ||||
| ``` | ||||
| 
 | ||||
| Add the highlighted lines to `index.js`: | ||||
| 
 | ||||
| ```js title="index.js" | ||||
| import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; | ||||
| 
 | ||||
| // highlight-start | ||||
| import { read, write } from 'xlsx'; | ||||
| import { getDocumentAsync } from 'expo-document-picker'; | ||||
| import { documentDirectory, readAsStringAsync, writeAsStringAsync } from 'expo-file-system'; | ||||
| 
 | ||||
| async function pickAndParse() { | ||||
|   const result = await getDocumentAsync({copyToCacheDirectory: true}); | ||||
|   const path = result.uri; | ||||
|   const res = await readAsStringAsync(path, { encoding: "base64" }); | ||||
|   return read(res, {type: 'base64'}); | ||||
| } | ||||
| 
 | ||||
| async function writeWorkbook(wb) { | ||||
|   const wbout = write(wb, {type:'base64', bookType:"xlsx"}); | ||||
|   const file = documentDirectory + "sheetjsw.xlsx"; | ||||
|   await writeAsStringAsync(file, wbout, { encoding: "base64" }); | ||||
|   return file; | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| const make_width = ws => { | ||||
| ``` | ||||
| 
 | ||||
|   </TabItem> | ||||
| </Tabs> | ||||
| 
 | ||||
| 
 | ||||
| 5) Refresh the app: | ||||
| 
 | ||||
| ```bash | ||||
| cd ios | ||||
| pod install | ||||
| cd .. | ||||
| ``` | ||||
| 
 | ||||
| Once refreshed, the development process must be restarted: | ||||
| 
 | ||||
| ```bash | ||||
| npx react-native run-ios | ||||
| ``` | ||||
| 
 | ||||
| **iOS Testing** | ||||
| 
 | ||||
| The app can be tested with the following sequence in the simulator: | ||||
| 
 | ||||
| - Download <https://sheetjs.com/pres.numbers> | ||||
| - In the simulator, click the Home icon to return to the home screen | ||||
| - Click on the "Files" icon | ||||
| - Click and drag `pres.numbers` from a Finder window into the simulator. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Make sure "On My iPhone" is highlighted and select "Save" | ||||
| - Click the Home icon again then select the `SheetJSRN` app | ||||
| - Click "Import data" and select `pres`: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| Once selected, the screen should refresh with new contents: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Click "Export data".  You will see a popup with a location: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Find the file and verify the contents are correct: | ||||
| 
 | ||||
| ```bash | ||||
| find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx | | ||||
|   while read x; do echo "$x"; npx xlsx-cli "$x"; done | ||||
| ``` | ||||
| 
 | ||||
| Once testing is complete, stop the simulator and the development process. | ||||
| 
 | ||||
| **Android Testing** | ||||
| 
 | ||||
| There are no Android-specific steps.  Emulator can be started with: | ||||
| 
 | ||||
| ```bash | ||||
| npx react-native run-android | ||||
| ``` | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| The app can be tested with the following sequence in the simulator: | ||||
| 
 | ||||
| - Download <https://sheetjs.com/pres.numbers> | ||||
| - Click and drag `pres.numbers` from a Finder window into the simulator. | ||||
| - Click "Import data" and select `pres.numbers`: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| Once selected, the screen should refresh with new contents: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Click "Export data".  You will see a popup with a location: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Pull the file from the simulator and verify the contents: | ||||
| 
 | ||||
| ```bash | ||||
| adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx | ||||
| npx xlsx-cli /tmp/sheetjsw.xlsx | ||||
| ``` | ||||
							
								
								
									
										350
									
								
								docz/docs/03-demos/02-mobile/02-nativescript.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										350
									
								
								docz/docs/03-demos/02-mobile/02-nativescript.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,350 @@ | ||||
| --- | ||||
| title: NativeScript | ||||
| pagination_prev: demos/salesforce | ||||
| pagination_next: demos/desktop/index | ||||
| sidebar_position: 2 | ||||
| sidebar_custom_props: | ||||
|   summary: JS + Native Elements | ||||
| --- | ||||
| 
 | ||||
| The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported | ||||
| from the main entrypoint or any script in the project. | ||||
| 
 | ||||
| :::warning Binary Data issues | ||||
| 
 | ||||
| NativeScript will not safely transmit binary or UTF-8 strings. XLSB, NUMBERS, | ||||
| XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled. | ||||
| 
 | ||||
| This is a known NativeScript bug. | ||||
| 
 | ||||
| This demo will focus on ASCII CSV files.  Once the bug is resolved, XLSX and | ||||
| other formats will be supported. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| ## Integration Details | ||||
| 
 | ||||
| The `@nativescript/core/file-system` package provides classes for file access. | ||||
| 
 | ||||
| Reading and writing data require a file handle.  The following snippet searches | ||||
| typical document folders for a specified filename: | ||||
| 
 | ||||
| ```ts | ||||
| import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| 
 | ||||
| function get_handle_for_filename(filename: string): File { | ||||
|   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | ||||
|   const url: string = path.normalize(target.path + "///" + filename); | ||||
|   return File.fromPath(url); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| The encoding `ISO_8859_1` spiritually resembles the `"binary"` SheetJS type | ||||
| 
 | ||||
| **Reading data** | ||||
| 
 | ||||
| `File#readText(encoding.ISO_8859_1)` returns strings compatible with `"binary"` | ||||
| 
 | ||||
| ```ts | ||||
| /* get binary string */ | ||||
| const bstr: string = await file.readText(encoding.ISO_8859_1); | ||||
| 
 | ||||
| /* read workbook */ | ||||
| const wb = read(bstr, { type: "binary" }); | ||||
| ``` | ||||
| 
 | ||||
| **Writing data** | ||||
| 
 | ||||
| `File#writeText` with the `ISO_8859_1` encoding accepts `"binary"` strings with | ||||
| the caveat listed in the warning at the top of this section: | ||||
| 
 | ||||
| ```ts | ||||
| /* generate binary string */ | ||||
| const bstr: string = write(wb, { bookType: 'csv', type: 'binary' }); | ||||
| 
 | ||||
| /* attempt to save binary string to file */ | ||||
| await file.writeText(bstr, encoding.ISO_8859_1); | ||||
| ``` | ||||
| 
 | ||||
| ## Demo | ||||
| 
 | ||||
| The demo builds off of the NativeScript + Angular example.  Familiarity with | ||||
| Angular and TypeScript is assumed. | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| This demo was tested on an Intel Mac on 2022 August 10.  NativeScript version | ||||
| (as verified with `ns --version`) is `8.3.2`.  The iOS simulator runs iOS 15.5 | ||||
| on an iPhone SE 3rd generation. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| <details><summary><b>Complete Example</b> (click to show)</summary> | ||||
| 
 | ||||
| 0) Follow the official Environment Setup instructions (tested with "MacOS + iOS") | ||||
| 
 | ||||
| 1) Create a skeleton NativeScript + Angular app: | ||||
| 
 | ||||
| ```bash | ||||
| ns create SheetJSNS --ng | ||||
| ``` | ||||
| 
 | ||||
| 2) Launch the app in the iOS simulator to verify that the demo built properly: | ||||
| 
 | ||||
| ```bash | ||||
| cd SheetJSNS | ||||
| ns run ios | ||||
| ``` | ||||
| 
 | ||||
| (this may take a while) | ||||
| 
 | ||||
| Once the simulator launches and the test app is displayed, end the script by | ||||
| selecting the terminal and entering the key sequence `CTRL + C` | ||||
| 
 | ||||
| 3) From the project folder, install the library: | ||||
| 
 | ||||
| ```bash | ||||
| npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | ||||
| ``` | ||||
| 
 | ||||
| 4) To confirm the library was loaded, change the title to show the version.  The | ||||
| differences are highlighted. | ||||
| 
 | ||||
| `src/app/item/items.component.ts` imports the version string to the component: | ||||
| 
 | ||||
| ```ts title="src/app/item/items.component.ts" | ||||
| // highlight-next-line | ||||
| import { version } from 'xlsx'; | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| 
 | ||||
| import { Item } from './item' | ||||
| import { ItemService } from './item.service' | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'ns-items', | ||||
|   templateUrl: './items.component.html', | ||||
| }) | ||||
| export class ItemsComponent implements OnInit { | ||||
|   items: Array<Item> | ||||
|   // highlight-next-line | ||||
|   version = `SheetJS - ${version}`; | ||||
| 
 | ||||
|   constructor(private itemService: ItemService) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.items = this.itemService.getItems() | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `src/app/item/items.component.html` references the version in the title: | ||||
| 
 | ||||
| ```xml title="src/app/item/items.component.html" | ||||
| <!-- highlight-next-line --> | ||||
| <ActionBar [title]="version"></ActionBar> | ||||
| 
 | ||||
| <GridLayout> | ||||
|   <ListView [items]="items"> | ||||
|     <ng-template let-item="item"> | ||||
|       <StackLayout [nsRouterLink]="['/item', item.id]"> | ||||
|         <Label [text]="item.name"></Label> | ||||
|       </StackLayout> | ||||
|     </ng-template> | ||||
|   </ListView> | ||||
| </GridLayout> | ||||
| ``` | ||||
| 
 | ||||
| Relaunch the app with `ns run ios` and the title bar should show the version. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 5) Add the Import and Export buttons to the template: | ||||
| 
 | ||||
| ```xml title="src/app/item/items.component.html" | ||||
| <ActionBar [title]="version"></ActionBar> | ||||
| 
 | ||||
| <!-- highlight-start --> | ||||
| <StackLayout> | ||||
|   <StackLayout orientation="horizontal"> | ||||
|     <Button text="Import File" (tap)="import()" style="padding: 10px"></Button> | ||||
|     <Button text="Export File" (tap)="export()" style="padding: 10px"></Button> | ||||
|   </StackLayout> | ||||
| <!-- highlight-end --> | ||||
|   <ListView [items]="items"> | ||||
|     <ng-template let-item="item"> | ||||
|       <StackLayout [nsRouterLink]="['/item', item.id]"> | ||||
|         <Label [text]="item.name"></Label> | ||||
|       </StackLayout> | ||||
|     </ng-template> | ||||
|   </ListView> | ||||
| <!-- highlight-next-line --> | ||||
| </StackLayout> | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="src/app/item/items.component.ts" | ||||
| // highlight-start | ||||
| import { version, utils, read, write } from 'xlsx'; | ||||
| import { Dialogs } from '@nativescript/core'; | ||||
| import { encoding } from '@nativescript/core/text'; | ||||
| import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| // highlight-end | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| 
 | ||||
| import { Item } from './item' | ||||
| import { ItemService } from './item.service' | ||||
| 
 | ||||
| // highlight-start | ||||
| function get_handle_for_filename(filename: string): [File, string] { | ||||
|   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | ||||
|   const url: string = path.normalize(target.path + "///" + filename); | ||||
|   return [File.fromPath(url), url]; | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'ns-items', | ||||
|   templateUrl: './items.component.html', | ||||
| }) | ||||
| export class ItemsComponent implements OnInit { | ||||
|   items: Array<Item> | ||||
|   version: string = `SheetJS - ${version}`; | ||||
| 
 | ||||
|   constructor(private itemService: ItemService) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.items = this.itemService.getItems() | ||||
|   } | ||||
| 
 | ||||
|   // highlight-start | ||||
|   /* Import button */ | ||||
|   async import() { | ||||
|   } | ||||
| 
 | ||||
|   /* Export button */ | ||||
|   async export() { | ||||
|   } | ||||
|   // highlight-end | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Restart the app process and two buttons should show up at the top: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 6) Implement import and export: | ||||
| 
 | ||||
| ```ts title="src/app/item/items.component.ts" | ||||
| import { version, utils, read, write } from 'xlsx'; | ||||
| import { Dialogs } from '@nativescript/core'; | ||||
| import { encoding } from '@nativescript/core/text'; | ||||
| import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| 
 | ||||
| import { Item } from './item' | ||||
| import { ItemService } from './item.service' | ||||
| 
 | ||||
| function get_handle_for_filename(filename: string): [File, string] { | ||||
|   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | ||||
|   const url: string = path.normalize(target.path + "///" + filename); | ||||
|   return [File.fromPath(url), url]; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'ns-items', | ||||
|   templateUrl: './items.component.html', | ||||
| }) | ||||
| export class ItemsComponent implements OnInit { | ||||
|   items: Array<Item> | ||||
|   version: string = `SheetJS - ${version}`; | ||||
| 
 | ||||
|   constructor(private itemService: ItemService) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.items = this.itemService.getItems() | ||||
|   } | ||||
| 
 | ||||
|   /* Import button */ | ||||
|   async import() { | ||||
|     // highlight-start | ||||
|     /* find appropriate path */ | ||||
|     const [file, url] = get_handle_for_filename("SheetJSNS.csv"); | ||||
| 
 | ||||
|     try { | ||||
|       /* get binary string */ | ||||
|       const bstr: string = await file.readText(encoding.ISO_8859_1); | ||||
| 
 | ||||
|       /* read workbook */ | ||||
|       const wb = read(bstr, { type: "binary" }); | ||||
| 
 | ||||
|       /* grab first sheet */ | ||||
|       const wsname: string = wb.SheetNames[0]; | ||||
|       const ws = wb.Sheets[wsname]; | ||||
| 
 | ||||
|       /* update table */ | ||||
|       this.items = utils.sheet_to_json<Item>(ws); | ||||
|       Dialogs.alert(`Attempting to read to ${filename} in ${url}`); | ||||
|     } catch(e) { Dialogs.alert(e.message); } | ||||
|     // highlight-end | ||||
|   } | ||||
| 
 | ||||
|   /* Export button */ | ||||
|   async export() { | ||||
|     // highlight-start | ||||
|     /* find appropriate path */ | ||||
|     const [file, url] = get_handle_for_filename("SheetJSNS.csv"); | ||||
| 
 | ||||
|     try { | ||||
|       /* create worksheet from data */ | ||||
|       const ws = utils.json_to_sheet(this.items); | ||||
| 
 | ||||
|       /* create workbook from worksheet */ | ||||
|       const wb = utils.book_new(); | ||||
|       utils.book_append_sheet(wb, ws, "Sheet1"); | ||||
| 
 | ||||
|       /* generate binary string */ | ||||
|       const wbout: string = write(wb, { bookType: 'csv', type: 'binary' }); | ||||
| 
 | ||||
|       /* attempt to save binary string to file */ | ||||
|       await file.writeText(wbout, encoding.ISO_8859_1); | ||||
|       Dialogs.alert(`Wrote to ${filename} in ${url}`); | ||||
|     } catch(e) { Dialogs.alert(e.message); } | ||||
|     // highlight-end | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Restart the app process. | ||||
| 
 | ||||
| **Testing** | ||||
| 
 | ||||
| The app can be tested with the following sequence in the simulator: | ||||
| 
 | ||||
| - Hit "Export File".  A dialog will print where the file was written | ||||
| 
 | ||||
| - Open that file with a text editor.  It will be a 3-column CSV: | ||||
| 
 | ||||
| ```csv | ||||
| id,name,role | ||||
| 1,Ter Stegen,Goalkeeper | ||||
| 3,Piqué,Defender | ||||
| 4,I. Rakitic,Midfielder | ||||
| ... | ||||
| ``` | ||||
| 
 | ||||
| After the header row, add the line `0,SheetJS,Library`: | ||||
| 
 | ||||
| ```csv | ||||
| id,name,role | ||||
| 0,SheetJS,Library | ||||
| 1,Ter Stegen,Goalkeeper | ||||
| 3,Piqué,Defender | ||||
| ... | ||||
| ``` | ||||
| 
 | ||||
| - Hit "Import File".  A dialog will print the path of the file that was read. | ||||
|   The first item in the list will change: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
							
								
								
									
										381
									
								
								docz/docs/03-demos/02-mobile/03-quasar.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										381
									
								
								docz/docs/03-demos/02-mobile/03-quasar.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,381 @@ | ||||
| --- | ||||
| title: Quasar | ||||
| pagination_prev: demos/salesforce | ||||
| pagination_next: demos/desktop/index | ||||
| sidebar_position: 3 | ||||
| sidebar_custom_props: | ||||
|   summary: VueJS + Web View | ||||
| --- | ||||
| 
 | ||||
| The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported | ||||
| from the main entrypoint or any script in the project. | ||||
| 
 | ||||
| This demo will use the Quasar ViteJS starter project with VueJS and Cordova. | ||||
| 
 | ||||
| ### Integration Details | ||||
| 
 | ||||
| The complete solution uses `cordova-plugin-file` for file operations.  It can | ||||
| be installed like any other Cordova plugin: | ||||
| 
 | ||||
| ```bash | ||||
| cd src-cordova | ||||
| cordova plugin add cordova-plugin-file | ||||
| cd .. | ||||
| ``` | ||||
| 
 | ||||
| #### Reading data | ||||
| 
 | ||||
| The `q-file` component presents an API reminiscent of File Input elements: | ||||
| 
 | ||||
| ```html | ||||
| <q-file label="Load File" filled label-color="orange" @input="updateFile"/> | ||||
| ``` | ||||
| 
 | ||||
| When binding to the `input` element, the callback receives an `Event` object: | ||||
| 
 | ||||
| ```ts | ||||
| import { read } from 'xlsx'; | ||||
| 
 | ||||
| // assuming `todos` is a standard VueJS `ref` | ||||
| async function updateFile(v) { try { | ||||
|   // `v.target.files[0]` is the desired file object | ||||
|   const files = (v.target as HTMLInputElement).files; | ||||
|   if(!files || files.length == 0) return; | ||||
| 
 | ||||
|   // read first file | ||||
|   const wb = read(await files[0].arrayBuffer()); | ||||
| 
 | ||||
|   // get data of first worksheet as an array of objects | ||||
|   const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); | ||||
| 
 | ||||
|   // update state | ||||
|   todos.value = data.map(row => ({id: row.Index, content: row.Name})); | ||||
| 
 | ||||
| } catch(e) { console.log(e); } } | ||||
| ``` | ||||
| 
 | ||||
| #### Writing data | ||||
| 
 | ||||
| The API is shaped like the File and Directory Entries API.  For clarity, since | ||||
| the code is a "pyramid of doom", the error handlers are omitted: | ||||
| 
 | ||||
| ```ts | ||||
| import { write } from 'xlsx'; | ||||
| 
 | ||||
| // on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array` | ||||
| const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"}); | ||||
| // Request filesystem access for persistent storage | ||||
| window.requestFileSystem(window.PERSISTENT, 0, function(fs) { | ||||
|   // Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary | ||||
|   fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => { | ||||
|     // Request a FileWriter for writing data | ||||
|     entry.createWriter(writer => { | ||||
|       // The FileWriter API needs an actual Blob | ||||
|       const data = new Blob([u8], {type: "application/vnd.ms-excel"}); | ||||
|       // This callback is called if the write is successful | ||||
|       writer.onwriteend = () => { | ||||
|         // TODO: show a dialog | ||||
|       }; | ||||
|       // writer.onerror will be invoked if there is an error in writing | ||||
| 
 | ||||
|       // write the data | ||||
|       writer.write(data); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### Demo | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| This demo was tested on an Intel Mac on 2022 August 14. Quasar version `2.7.7`. | ||||
| The iOS simulator runs iOS 15.5 on an iPhone SE 3rd generation. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| The demo draws from the ViteJS example.  Familiarity with VueJS and TypeScript | ||||
| is assumed. | ||||
| 
 | ||||
| <details><summary><b>Complete Example</b> (click to show)</summary> | ||||
| 
 | ||||
| 0) Ensure all of the dependencies are installed.  Install the CLI globally: | ||||
| 
 | ||||
| ```bash | ||||
| npm i -g @quasar/cli cordova | ||||
| ``` | ||||
| 
 | ||||
| (you may need to run `sudo npm i -g` if there are permission issues) | ||||
| 
 | ||||
| 1) Create a new app: | ||||
| 
 | ||||
| ```bash | ||||
| npm init quasar | ||||
| ``` | ||||
| 
 | ||||
| <!-- spellchecker-disable --> | ||||
| 
 | ||||
| When prompted: | ||||
| 
 | ||||
| - "What would you like to build?": `App with Quasar CLI` | ||||
| - "Project folder": `SheetJSQuasar` | ||||
| - "Pick Quasar version": `Quasar v2 (Vue 3 | latest and greatest)` | ||||
| - "Pick script type": `Typescript` | ||||
| - "Pick Quasar App CLI variant": `Quasar App CLI with Vite` | ||||
| - "Package name": (just press enter, it will use the default `sheetjsquasar` | ||||
| - "Project product name": `SheetJSQuasar` | ||||
| - "Project description": `SheetJS + Quasar` | ||||
| - "Author": (just press enter, it will use your git config settings) | ||||
| - "Pick a Vue component style": `Composition API` | ||||
| - "Pick your CSS preprocessor": `None` | ||||
| - "Check the features needed for your project": Deselect everything | ||||
| - "Install project dependencies": `No` | ||||
| 
 | ||||
| 2) Install dependencies: | ||||
| 
 | ||||
| <!-- spellchecker-enable --> | ||||
| 
 | ||||
| ```bash | ||||
| cd SheetJSQuasar | ||||
| npm i | ||||
| npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | ||||
| ``` | ||||
| 
 | ||||
| 3) Set up Cordova: | ||||
| 
 | ||||
| ```bash | ||||
| quasar mode add cordova | ||||
| ``` | ||||
| 
 | ||||
| When prompted, enter the app id `org.sheetjs.quasar`. | ||||
| 
 | ||||
| It will create a new `src-cordova` folder. Continue in that folder: | ||||
| 
 | ||||
| ```bash | ||||
| cd src-cordova | ||||
| cordova platform add ios | ||||
| cordova plugin add cordova-plugin-wkwebview-engine | ||||
| cordova plugin add cordova-plugin-file | ||||
| ``` | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| If there is an error `Could not load API for iOS project`, it needs to be reset: | ||||
| 
 | ||||
| ```bash | ||||
| cordova platform rm ios | ||||
| cordova platform add ios | ||||
| cordova plugin add cordova-plugin-file | ||||
| ``` | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| Return to the project directory: | ||||
| 
 | ||||
| ```bash | ||||
| cd .. | ||||
| ``` | ||||
| 
 | ||||
| 4) Start the development server: | ||||
| 
 | ||||
| ```bash | ||||
| quasar dev -m ios | ||||
| ``` | ||||
| 
 | ||||
| :::caution | ||||
| 
 | ||||
| If the app is blank or not refreshing, delete the app and close the simulator, | ||||
| then restart the development process. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 
 | ||||
| 5) Add the Dialog plugin to `quasar.config.js`: | ||||
| 
 | ||||
| ```js title="quasar.config.js" | ||||
|       // Quasar plugins | ||||
|       // highlight-next-line | ||||
|       plugins: ['Dialog'] | ||||
| ``` | ||||
| 
 | ||||
| 6) In the template section of `src/pages/IndexPage.vue`, add a Save button and | ||||
|    a Load file picker component at the bottom of the page: | ||||
| 
 | ||||
| ```html title="src/pages/IndexPage.vue" | ||||
|     <!-- highlight-start --> | ||||
|     <q-btn-group> | ||||
|       <q-file label="Load File" filled label-color="orange" @input="updateFile"/> | ||||
|       <q-btn label="Save File" @click="saveFile" /> | ||||
|     </q-btn-group> | ||||
|     <!-- highlight-end --> | ||||
|   </q-page> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| This uses two functions that should be added to the component script: | ||||
| 
 | ||||
| ```ts title="src/pages/IndexPage.vue" | ||||
|     const meta = ref<Meta>({ | ||||
|       totalCount: 1200 | ||||
|     }); | ||||
| // highlight-start | ||||
|     function saveFile() { | ||||
|     } | ||||
|     async function updateFile(v) { | ||||
|     } | ||||
|     return { todos, meta, saveFile, updateFile }; | ||||
| // highlight-end | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| The app should now show two buttons at the bottom: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| :::caution | ||||
| 
 | ||||
| If the app is blank or not refreshing, delete the app and close the simulator, | ||||
| then restart the development process. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| 7) Wire up the `updateFile` function: | ||||
| 
 | ||||
| ```ts title="src/pages/IndexPage.vue" | ||||
| import { defineComponent, ref } from 'vue'; | ||||
| // highlight-start | ||||
| import { read, write, utils } from 'xlsx'; | ||||
| import { useQuasar } from 'quasar'; | ||||
| // highlight-end | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| // ... | ||||
| // highlight-start | ||||
|     const $q = useQuasar(); | ||||
|     function dialogerr(e) { $q.dialog({title: "Error!", message: e.message || String(e)}); } | ||||
| // highlight-end | ||||
|     function saveFile() { | ||||
|     } | ||||
|     async function updateFile(v) { | ||||
| // highlight-start | ||||
|       try { | ||||
|         const files = (v.target as HTMLInputElement).files; | ||||
|         if(!files || files.length == 0) return; | ||||
| 
 | ||||
|         const wb = read(await files[0].arrayBuffer()); | ||||
| 
 | ||||
|         const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); | ||||
|         todos.value = data.map(row => ({id: row.Index, content: row.Name})); | ||||
|       } catch(e) { dialogerr(e); } | ||||
| // highlight-end | ||||
|     } | ||||
| ``` | ||||
| 
 | ||||
| To test that reading works: | ||||
| 
 | ||||
| - Download <https://sheetjs.com/pres.numbers> | ||||
| - In the simulator, click the Home icon to return to the home screen | ||||
| - Click on the "Files" icon | ||||
| - Click and drag `pres.numbers` from a Finder window into the simulator. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Make sure "On My iPhone" is highlighted and select "Save" | ||||
| - Click the Home icon again then select the `SheetJSQuasar` app | ||||
| - Click the "Load" button, then select "Choose File" and select `pres`: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| Once selected, the screen should refresh with new contents: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 8) Wire up the `saveFile` function: | ||||
| 
 | ||||
| ```js | ||||
|     function saveFile() { | ||||
| // highlight-start | ||||
|       /* generate workbook from state */ | ||||
|       const ws = utils.json_to_sheet(todos.value); | ||||
|       const wb = utils.book_new(); | ||||
|       utils.book_append_sheet(wb, ws, "SheetJSQuasar"); | ||||
|       const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"}); | ||||
| 
 | ||||
|       /* save to file */ | ||||
|       window.requestFileSystem(window.PERSISTENT, 0, function(fs) { | ||||
|         try { | ||||
|           fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => { | ||||
|             const msg = `File stored at ${$q.cordova.file.documentsDirectory} ${entry.fullPath}`; | ||||
|             entry.createWriter(writer => { | ||||
|               try { | ||||
|                 const data = new Blob([u8], {type: "application/vnd.ms-excel"}); | ||||
|                 writer.onwriteend = () => { | ||||
|                   try { | ||||
|                     $q.dialog({title: "Success!", message: msg}); | ||||
|                   } catch(e) { dialogerr(e); } | ||||
|                 }; | ||||
|                 writer.onerror = dialogerr; | ||||
|                 writer.write(data); | ||||
|               } catch(e) { dialogerr(e); } | ||||
|             }, dialogerr); | ||||
|           }, dialogerr); | ||||
|         } catch(e) { dialogerr(e) } | ||||
|       }, dialogerr); | ||||
| // highlight-end | ||||
|     } | ||||
| ``` | ||||
| 
 | ||||
| The page should revert to the old contents. | ||||
| 
 | ||||
| To test that writing works: | ||||
| 
 | ||||
| - Click "Save File".  You will see a popup with a location: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| - Find the file and verify the contents are correct.  Run in a new terminal: | ||||
| 
 | ||||
| ```bash | ||||
| find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx | | ||||
|   while read x; do echo "$x"; npx xlsx-cli "$x"; done | ||||
| ``` | ||||
| 
 | ||||
| Since the contents reverted, you should see | ||||
| 
 | ||||
| ``` | ||||
| SheetJSQuasar | ||||
| id,content | ||||
| 1,ct1 | ||||
| 2,ct2 | ||||
| 3,ct3 | ||||
| 4,ct4 | ||||
| 5,ct5 | ||||
| ``` | ||||
| 
 | ||||
| - Use "Load File" to select `pres.numbers` again.  Wait for the app to refresh. | ||||
| 
 | ||||
| - Click "Save File", then re-run the command: | ||||
| 
 | ||||
| ```bash | ||||
| find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx | | ||||
|   while read x; do echo "$x"; npx xlsx-cli "$x"; done | ||||
| ``` | ||||
| 
 | ||||
| The contents from `pres.numbers` should show up now, with a new header row: | ||||
| 
 | ||||
| ``` | ||||
| SheetJSQuasar | ||||
| id,content | ||||
| 42,Bill Clinton | ||||
| 43,GeorgeW Bush | ||||
| 44,Barack Obama | ||||
| 45,Donald Trump | ||||
| 46,Joseph Biden | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
							
								
								
									
										155
									
								
								docz/docs/03-demos/02-mobile/04-ionic.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										155
									
								
								docz/docs/03-demos/02-mobile/04-ionic.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| --- | ||||
| title: Ionic | ||||
| pagination_prev: demos/salesforce | ||||
| pagination_next: demos/desktop/index | ||||
| sidebar_position: 4 | ||||
| sidebar_custom_props: | ||||
|   summary: Native Components + Web View | ||||
| --- | ||||
| 
 | ||||
| The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported | ||||
| from the main entrypoint or any script in the project. | ||||
| 
 | ||||
| :::warning Telemetry | ||||
| 
 | ||||
| Before starting this demo, manually disable telemetry.  On Linux and MacOS: | ||||
| 
 | ||||
| ```bash | ||||
| rm -rf ~/.ionic/ | ||||
| mkdir ~/.ionic | ||||
| cat <<EOF > ~/.ionic/config.json | ||||
| { | ||||
|   "version": "6.20.1", | ||||
|   "telemetry": false, | ||||
|   "npmClient": "npm" | ||||
| } | ||||
| EOF | ||||
| npx @capacitor/cli telemetry off | ||||
| ``` | ||||
| 
 | ||||
| To verify telemetry was disabled: | ||||
| 
 | ||||
| ```bash | ||||
| npx @ionic/cli config get -g telemetry | ||||
| npx @capacitor/cli telemetry | ||||
| ``` | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| ## Cordova | ||||
| 
 | ||||
| :::caution | ||||
| 
 | ||||
| The latest version of Ionic uses CapacitorJS. These notes are for Cordova apps. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| `Array<Array<any>>` neatly maps to a table with `ngFor`: | ||||
| 
 | ||||
| ```html | ||||
| <ion-grid> | ||||
|   <ion-row *ngFor="let row of data"> | ||||
|     <ion-col *ngFor="let val of row"> | ||||
|       {{val}} | ||||
|     </ion-col> | ||||
|   </ion-row> | ||||
| </ion-grid> | ||||
| ``` | ||||
| 
 | ||||
| `@ionic-native/file` reads and writes files on devices. `readAsArrayBuffer` | ||||
| returns `ArrayBuffer` objects suitable for `array` type, and `array` type can | ||||
| be converted to blobs that can be exported with `writeFile`: | ||||
| 
 | ||||
| ```ts | ||||
| /* read a workbook */ | ||||
| const ab: ArrayBuffer = await this.file.readAsArrayBuffer(url, filename); | ||||
| const wb: XLSX.WorkBook = XLSX.read(bstr, {type: 'array'}); | ||||
| 
 | ||||
| /* write a workbook */ | ||||
| const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); | ||||
| let blob = new Blob([wbout], {type: 'application/octet-stream'}); | ||||
| this.file.writeFile(url, filename, blob, {replace: true}); | ||||
| ``` | ||||
| 
 | ||||
| ## Demo | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| This demo was tested on an Intel Mac on 2022 August 18 with Cordova. | ||||
| The file integration uses `@ionic-native/file` version `5.36.0`. | ||||
| 
 | ||||
| The iOS simulator runs iOS 15.5 on an iPod Touch 7th Gen. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| <details><summary><b>Complete Example</b> (click to show)</summary> | ||||
| 
 | ||||
| 0) Disable telemetry as noted in the warning. | ||||
| 
 | ||||
| Install required global dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| npm i -g cordova-res @angular/cli native-run | ||||
| ``` | ||||
| 
 | ||||
| Follow the [React Native demo](#demo) to ensure iOS and Android sims are ready. | ||||
| 
 | ||||
| 
 | ||||
| 1) Create a new project: | ||||
| 
 | ||||
| ```bash | ||||
| npx @ionic/cli start SheetJSIonic blank --type angular --cordova --quiet --no-git --no-link --confirm | ||||
| ``` | ||||
| 
 | ||||
| If a prompt discusses Cordova and Capacitor, enter `Yes` to continue. | ||||
| 
 | ||||
| If a prompt asks about creating an Ionic account, enter `N` to opt out. | ||||
| 
 | ||||
| 2) Set up Cordova: | ||||
| 
 | ||||
| ```bash | ||||
| npx @ionic/cli cordova platform add ios --confirm | ||||
| npx @ionic/cli cordova plugin add cordova-plugin-file | ||||
| npm install --save @ionic-native/core @ionic-native/file @ionic/cordova-builders | ||||
| ``` | ||||
| 
 | ||||
| 3) Install dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | ||||
| ``` | ||||
| 
 | ||||
| 4) Add `@ionic-native/file` to the module.  Differences highlighted below: | ||||
| 
 | ||||
| ```ts title="src/app/app.module.ts" | ||||
| import { AppComponent } from './app.component'; | ||||
| import { AppRoutingModule } from './app-routing.module'; | ||||
| 
 | ||||
| // highlight-next-line | ||||
| import { File } from '@ionic-native/file/ngx'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   declarations: [AppComponent], | ||||
|   imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], | ||||
| 
 | ||||
|   // highlight-next-line | ||||
|   providers: [File, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], | ||||
|   bootstrap: [AppComponent], | ||||
| }) | ||||
| export class AppModule {} | ||||
| ``` | ||||
| 
 | ||||
| 5) Download [`home.page.ts`](pathname:///ionic/home.page.ts) and replace: | ||||
| 
 | ||||
| ```bash | ||||
| curl -o src/app/home/home.page.ts -L https://docs.sheetjs.com/ionic/home.page.ts | ||||
| ``` | ||||
| 
 | ||||
| 6) Test the app: | ||||
| 
 | ||||
| ```bash | ||||
| npx @ionic/cli cordova emulate ios | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
							
								
								
									
										192
									
								
								docz/docs/03-demos/02-mobile/05-capacitor.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										192
									
								
								docz/docs/03-demos/02-mobile/05-capacitor.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,192 @@ | ||||
| --- | ||||
| title: CapacitorJS | ||||
| pagination_prev: demos/salesforce | ||||
| pagination_next: demos/desktop/index | ||||
| sidebar_position: 5 | ||||
| sidebar_custom_props: | ||||
|   summary: JS + Web View | ||||
| --- | ||||
| 
 | ||||
| ## CapacitorJS | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| This demo was tested on an Intel Mac on 2022 August 26 with Svelte. | ||||
| 
 | ||||
| The iOS simulator runs iOS 15.5 on an iPhone 13 Pro Max. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| :::warning Telemetry | ||||
| 
 | ||||
| Before starting this demo, manually disable telemetry.  On Linux and MacOS: | ||||
| 
 | ||||
| ```bash | ||||
| npx @capacitor/cli telemetry off | ||||
| ``` | ||||
| 
 | ||||
| To verify telemetry was disabled: | ||||
| 
 | ||||
| ```bash | ||||
| npx @capacitor/cli telemetry | ||||
| ``` | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| ### Integration Details | ||||
| 
 | ||||
| This example uses Svelte, but the same principles apply to other frameworks. | ||||
| 
 | ||||
| #### Reading data | ||||
| 
 | ||||
| The standard HTML5 File Input element logic works in CapacitorJS: | ||||
| 
 | ||||
| ```html | ||||
| <script> | ||||
| import { read, utils } from 'xlsx'; | ||||
| 
 | ||||
| let html = ""; | ||||
| 
 | ||||
| /* show file picker, read file, load table */ | ||||
| async function importFile(evt) { | ||||
|   // highlight-start | ||||
|   const f = evt.target.files[0]; | ||||
|   const wb = read(await f.arrayBuffer()); | ||||
|   // highlight-end | ||||
|   const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet | ||||
|   html = utils.sheet_to_html(ws); // generate HTML and update state | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <main> | ||||
|   <!-- highlight-next-line --> | ||||
|   <input type="file" on:change={importFile}/> | ||||
|   <div bind:this={tbl}>{@html html}</div> | ||||
| </main> | ||||
| ``` | ||||
| 
 | ||||
| #### Writing data | ||||
| 
 | ||||
| `@capacitor/filesystem` can write Base64 strings: | ||||
| 
 | ||||
| ```html | ||||
| <script> | ||||
| import { Filesystem, Directory } from '@capacitor/filesystem'; | ||||
| import { utils, writeXLSX } from 'xlsx'; | ||||
| 
 | ||||
| let html = ""; | ||||
| let tbl; | ||||
| 
 | ||||
| /* get state data and export to XLSX */ | ||||
| async function exportFile() { | ||||
|   const elt = tbl.getElementsByTagName("TABLE")[0]; | ||||
|   const wb = utils.table_to_book(elt); | ||||
|   /* generate Base64 string for Capacitor */ | ||||
|   // highlight-start | ||||
|   const data = writeXLSX(wb, { type: "base64" }); | ||||
|   await Filesystem.writeFile({ | ||||
|     data, | ||||
|     path: "SheetJSCap.xlsx", | ||||
|     directory: Directory.Documents | ||||
|   }); // write file | ||||
|   // highlight-end | ||||
| } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <main> | ||||
|   <button on:click={exportFile}>Export XLSX</button> | ||||
|   <div bind:this={tbl}>{@html html}</div> | ||||
| </main> | ||||
| ``` | ||||
| 
 | ||||
| ### Demo | ||||
| 
 | ||||
| <details><summary><b>Complete Example</b> (click to show)</summary> | ||||
| 
 | ||||
| 0) Disable telemetry as noted in the warning. | ||||
| 
 | ||||
| Follow the [React Native demo](#demo) to ensure iOS and Android sims are ready. | ||||
| 
 | ||||
| 
 | ||||
| 1) Create a new Svelte project: | ||||
| 
 | ||||
| ```bash | ||||
| npm create vite@latest sheetjs-cap -- --template svelte | ||||
| cd sheetjs-cap | ||||
| ``` | ||||
| 
 | ||||
| 2) Install dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | ||||
| npm i --save @capacitor/core @capacitor/cli @capacitor/ios @capacitor/filesystem | ||||
| ``` | ||||
| 
 | ||||
| 3) Create CapacitorJS structure: | ||||
| 
 | ||||
| ```bash | ||||
| npx cap init sheetjs-cap com.sheetjs.cap --web-dir=dist | ||||
| npx cap add ios | ||||
| ``` | ||||
| 
 | ||||
| 4) Replace the contents of `src/App.svelte` with the following: | ||||
| 
 | ||||
| ```html title="src/App.svelte" | ||||
| <script> | ||||
| import { Filesystem, Directory, Encoding } from '@capacitor/filesystem'; | ||||
| import { onMount } from 'svelte'; | ||||
| import { read, utils, version, writeXLSX } from 'xlsx'; | ||||
| 
 | ||||
| let html = ""; | ||||
| let tbl; | ||||
| 
 | ||||
| /* Fetch and update the state once */ | ||||
| onMount(async() => { | ||||
|   const f = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer(); | ||||
|   const wb = read(f); // parse the array buffer | ||||
|   const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet | ||||
|   html = utils.sheet_to_html(ws); // generate HTML and update state | ||||
| }); | ||||
| 
 | ||||
| /* get state data and export to XLSX */ | ||||
| async function exportFile() { | ||||
|   const elt = tbl.getElementsByTagName("TABLE")[0]; | ||||
|   const wb = utils.table_to_book(elt); | ||||
|   /* generate Base64 string for Capacitor */ | ||||
|   const data = writeXLSX(wb, { type: "base64" }); | ||||
|   /* write */ | ||||
|   await Filesystem.writeFile({ | ||||
|     path: "SheetJSCap.xlsx", | ||||
|     data, | ||||
|     directory: Directory.Documents | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /* show file picker, read file, load table */ | ||||
| async function importFile(evt) { | ||||
|   const f = evt.target.files[0]; | ||||
|   const wb = read(await f.arrayBuffer()); | ||||
|   const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet | ||||
|   html = utils.sheet_to_html(ws); // generate HTML and update state | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <main> | ||||
|   <h3>SheetJS × CapacitorJS { version }</h3> | ||||
|   <input type="file" on:change={importFile}/> | ||||
|   <button on:click={exportFile}>Export XLSX</button> | ||||
|   <div bind:this={tbl}>{@html html}</div> | ||||
| </main> | ||||
| ``` | ||||
| 
 | ||||
| 5) Test the app: | ||||
| 
 | ||||
| ```bash | ||||
| npm run build; npx cap sync; npx cap run ios | ||||
| ``` | ||||
| 
 | ||||
| There are 3 steps: build the Svelte app, sync with CapacitorJS, and run sim. | ||||
| This sequence must be run every time to ensure changes are propagated. | ||||
| 
 | ||||
| </details> | ||||
							
								
								
									
										4
									
								
								docz/docs/03-demos/02-mobile/_category_.json
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										4
									
								
								docz/docs/03-demos/02-mobile/_category_.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| { | ||||
|   "label": "iOS and Android Apps", | ||||
|   "position": 3 | ||||
| } | ||||
							
								
								
									
										62
									
								
								docz/docs/03-demos/02-mobile/index.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										62
									
								
								docz/docs/03-demos/02-mobile/index.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| --- | ||||
| title: iOS and Android Apps | ||||
| pagination_prev: demos/salesforce | ||||
| pagination_next: demos/desktop/index | ||||
| --- | ||||
| 
 | ||||
| import DocCardList from '@theme/DocCardList'; | ||||
| import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; | ||||
| 
 | ||||
| Many mobile app frameworks mix JavaScript / CSS / HTML5 concepts with native | ||||
| extensions and libraries to create a hybrid development experience.  Developers | ||||
| well-versed in web technologies can now build actual mobile applications that | ||||
| run on iOS and Android! | ||||
| 
 | ||||
| :::warning | ||||
| 
 | ||||
| **The ecosystem has broken backwards-compatibility many times!** | ||||
| 
 | ||||
| iOS and Android, as well as the underlying JavaScript frameworks, make breaking | ||||
| changes regularly.  The demos were tested against emulators / real devices at | ||||
| some point in time.  A framework or OS change can render the demos inoperable. | ||||
| 
 | ||||
| MacOS is required for the iOS demos.  The Android demos were tested on MacOS. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| The ["JavaScript Engines"](/docs/demos/engines) section includes samples for JS | ||||
| engines used in the mobile app frameworks.  SheetJS libraries have been tested | ||||
| in the relevant engines and should "just work" with some caveats. | ||||
| 
 | ||||
| Demos for common tools are included in separate pages.  Each demo section will | ||||
| mention test dates and platform versions. | ||||
| 
 | ||||
| <ul>{useCurrentSidebarCategory().items.map((item, index) => { | ||||
|   const listyle = (item.customProps?.icon) ? { | ||||
|     listStyleImage: `url("${item.customProps.icon}")` | ||||
|   } : {}; | ||||
|   return (<li style={listyle} {...(item.customProps?.class ? {className: item.customProps.class}: {})}> | ||||
|     <a href={item.href}>{item.label}</a>{item.customProps?.summary && (" - " + item.customProps.summary)} | ||||
|   </li>); | ||||
| })}</ul> | ||||
| 
 | ||||
| :::note Recommendation | ||||
| 
 | ||||
| React Native is extremely popular and is the recommended choice for greenfield | ||||
| projects that can use community modules.  However, its "lean core" approach | ||||
| forces developers to learn iOS/Android programming or use community modules to | ||||
| provide basic app features. | ||||
| 
 | ||||
| The original Web View framework was PhoneGap/Cordova.  The modern frameworks | ||||
| are built atop Cordova.  Cordova is waning in popularity but it has a deep | ||||
| library of community modules to solve many problems. | ||||
| 
 | ||||
| Before creating a new app, it is important to identify what features the app | ||||
| should support and investigate community modules.  If there are popular modules | ||||
| for features that must be included, or for teams that are comfortable with | ||||
| native app development, React Native is the obvious choice. | ||||
| 
 | ||||
| NativeScript is not recommended at this time.  There are known bugs related to | ||||
| binary data processing. The demo only supports plaintext file formats like CSV. | ||||
| 
 | ||||
| ::: | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: Electron | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| sidebar_position: 1 | ||||
| sidebar_custom_props: | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: NW.js | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| sidebar_position: 2 | ||||
| sidebar_custom_props: | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: Wails | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| sidebar_position: 3 | ||||
| sidebar_custom_props: | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: Tauri | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| sidebar_position: 4 | ||||
| sidebar_custom_props: | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: NeutralinoJS | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| sidebar_position: 5 | ||||
| sidebar_custom_props: | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: React Native for Desktop | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| sidebar_position: 6 | ||||
| sidebar_custom_props: | ||||
| @ -13,7 +13,7 @@ import TabItem from '@theme/TabItem'; | ||||
| :::note | ||||
| 
 | ||||
| This section covers React Native for desktop applications.  For iOS and Android | ||||
| applications, [check the mobile demo](/docs/demos/mobile) | ||||
| applications, [check the mobile demo](/docs/demos/mobile/reactnative) | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| --- | ||||
| title: Desktop Applications | ||||
| pagination_prev: demos/mobile | ||||
| pagination_prev: demos/mobile/index | ||||
| pagination_next: demos/grid | ||||
| --- | ||||
| 
 | ||||
|  | ||||
| @ -14,8 +14,8 @@ and TypeScript familiarity is assumed. | ||||
| 
 | ||||
| Other demos cover general Angular deployments, including: | ||||
| 
 | ||||
| - [iOS and Android applications powered by NativeScript](/docs/demos/mobile#nativescript) | ||||
| - [iOS and Android applications powered by Ionic](/docs/demos/mobile#ionic) | ||||
| - [iOS and Android applications powered by NativeScript](/docs/demos/mobile/nativescript) | ||||
| - [iOS and Android applications powered by Ionic](/docs/demos/mobile/ionic) | ||||
| 
 | ||||
| :::warning | ||||
| 
 | ||||
|  | ||||
| @ -10,8 +10,8 @@ familiarity is assumed. | ||||
| Other demos cover general React deployments, including: | ||||
| 
 | ||||
| - [Static Site Generation powered by NextJS](/docs/demos/content#nextjs) | ||||
| - [iOS and Android applications powered by React Native](/docs/demos/mobile#react-native) | ||||
| - [Desktop application powered by React-Native-Windows](/docs/demos/desktop#react-native-windows) | ||||
| - [iOS and Android applications powered by React Native](/docs/demos/mobile/reactnative) | ||||
| - [Desktop application powered by React Native Windows + macOS](/docs/demos/desktop/reactnative) | ||||
| - [React Data Grid UI component](/docs/demos/grid#react-data-grid) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -10,8 +10,8 @@ Components (SFC) and VueJS familiarity is assumed. | ||||
| Other demos cover general VueJS deployments, including: | ||||
| 
 | ||||
| - [Static Site Generation powered by NuxtJS](/docs/demos/content#nuxtjs) | ||||
| - [iOS and Android applications powered by Quasar](/docs/demos/mobile#quasar) | ||||
| - [Desktop application powered by Tauri](/docs/demos/desktop#tauri) | ||||
| - [iOS and Android applications powered by Quasar](/docs/demos/mobile/quasar) | ||||
| - [Desktop application powered by Tauri](/docs/demos/desktop/tauri) | ||||
| - [`vue3-table-lite` UI component](/docs/demos/grid#vue3-table-lite) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -9,8 +9,8 @@ familiarity is assumed. | ||||
| 
 | ||||
| Other demos cover general Svelte deployments, including: | ||||
| 
 | ||||
| - [iOS applications powered by CapacitorJS](/docs/demos/mobile#capacitorjs) | ||||
| - [Desktop application powered by Wails](/docs/demos/desktop#wails) | ||||
| - [iOS applications powered by CapacitorJS](/docs/demos/mobile/capacitor) | ||||
| - [Desktop application powered by Wails](/docs/demos/desktop/wails) | ||||
| 
 | ||||
| 
 | ||||
| ## Installation | ||||
|  | ||||
| @ -38,6 +38,14 @@ run in the web browser, demos will include interactive examples. | ||||
| - [`angular-ui-grid`](/docs/demos/grid#angular-ui-grid) | ||||
| - [`material ui`](/docs/demos/grid#material-ui-table) | ||||
| 
 | ||||
| ### iOS and Android Mobile Apps | ||||
| 
 | ||||
| - [`React Native`](/docs/demos/mobile/reactnative) | ||||
| - [`NativeScript`](/docs/demos/mobile/nativescript) | ||||
| - [`Quasar`](/docs/demos/mobile/quasar) | ||||
| - [`Ionic`](/docs/demos/mobile/ionic) | ||||
| - [`CapacitorJS`](/docs/demos/mobile/capacitor) | ||||
| 
 | ||||
| ### Desktop App Frameworks | ||||
| 
 | ||||
| - [`Electron`](/docs/demos/desktop/electron) | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| <head> | ||||
| <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | ||||
| <meta http-equiv="Content-Security-Policy" content="script-src 'self' https:"> | ||||
| <meta name="robots" content="noindex"> | ||||
| <title>SheetJS Electron Demo</title> | ||||
| <style> | ||||
| #drop{ | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user