diff --git a/docz/docs/03-demos/05-mobile/01-reactnative.md b/docz/docs/03-demos/05-mobile/01-reactnative.md index 5d795b4..68eb6f4 100644 --- a/docz/docs/03-demos/05-mobile/01-reactnative.md +++ b/docz/docs/03-demos/05-mobile/01-reactnative.md @@ -1,5 +1,7 @@ --- title: React Native +sidebar_label: React Native +description: Build data-intensive mobile apps with React Native. Seamlessly integrate spreadsheets into your app using SheetJS. Securely process and generate Excel files in the field. pagination_prev: demos/static/index pagination_next: demos/desktop/index sidebar_position: 1 @@ -7,13 +9,26 @@ sidebar_custom_props: summary: React + Native Rendering --- +# Sheets on the Go with React Native + import current from '/version.js'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; -The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported -from the main `App.js` entrypoint or any script in the project. +[React Native](https://reactnative.dev/) is a mobile app framework. It builds +iOS and Android apps that use JavaScript for describing layouts and events. + +[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing +data from spreadsheets. + +This demo uses React Native and SheetJS to process and generate spreadsheets. +We'll explore how to load SheetJS in a React Native app in a few ways: + +- ["Fetching Remote Data"](#fetching-remote-data) uses the built-in `fetch` to +download and parse remote workbook files. +- ["Local Files"](#local-files) uses native libraries to read and write files on +the device. The "Complete Example" creates an app that looks like the screenshots below: @@ -30,17 +45,9 @@ The "Complete Example" creates an app that looks like the screenshots below: -["Fetching Remote Data"](#fetching-remote-data) uses the built-in `fetch` to -download and parse remote workbook files. +:::caution pass -["Native Libraries"](#native-libraries) uses native libraries to read and write -files in the local device. - -:::caution - -**Before reading this demo, follow the official React Native CLI Guide!** - -Development Environment Guide: +**Before testing this demo, follow the official React Native CLI Guide!**[^1] Follow the instructions for iOS (requires macOS) and for Android. They will cover installation and system configuration. You should be able to build and run @@ -48,42 +55,178 @@ a sample app in the Android and the iOS (if applicable) simulators. ::: +## Integration Details + +The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be +imported from the main `App.js` entrypoint or any script in the project. + +### Internal State + +For simplicity, this demo uses an "Array of Arrays"[^2] as the internal state. + +
SpreadsheetArray of Arrays
+ +![`pres.xlsx` data](pathname:///pres.png) + + + +```js +[ + ["Name", "Index"], + ["Bill Clinton", 42], + ["GeorgeW Bush", 43], + ["Barack Obama", 44], + ["Donald Trump", 45], + ["Joseph Biden", 46] +] +``` + +
+ +Each array within the structure corresponds to one row. + +This demo also keeps track of the column widths as a single array of numbers. +The widths are used by the display component. + +_Complete State_ + +The complete state is initialized with the following snippet: + +```js +const [data, setData] = useState([ + "SheetJS".split(""), + [5,4,3,3,7,9,5], + [8,6,7,5,3,0,9] +]); +const [widths, setWidths] = useState(Array.from({length:7}, () => 20)); +``` + + +#### Updating State + +Starting from a SheetJS worksheet object, `sheet_to_json`[^3] with the `header` +option can generate an array of arrays: + +```js +/* assuming `wb` is a SheetJS workbook */ +function update_state(wb) { + /* convert first worksheet to AOA */ + const wsname = wb.SheetNames[0]; + const ws = wb.Sheets[wsname]; + const data = utils.sheet_to_json(ws, {header:1}); + + /* update state */ + setData(data); + + /* update column widths */ + setWidths(make_width(data)); +} +``` + +_Calculating Column Widths_ + +Column widths can be calculated by walking each column and calculating the max +data width. Using the array of arrays: + +```js +/* this function takes an array of arrays and generates widths */ +function make_width(aoa) { + /* walk each row */ + aoa.forEach((r) => { + /* walk each column */ + r.forEach((c, C) => { + /* update column width based on the length of the cell contents */ + res[C] = Math.max(res[C]||60, String(c).length * 10); + }); + }); + /* use a default value for columns with no data */ + for(let C = 0; C < res.length; ++C) if(!res[C]) res[C] = 60; + return res; +} +``` + +#### Exporting State + +`aoa_to_sheet`[^4] builds a SheetJS worksheet object from the array of arrays: + +```js +/* generate a SheetJS workbook from the state */ +function export_state() { + /* convert AOA back to worksheet */ + const ws = utils.aoa_to_sheet(data); + + /* build new workbook */ + const wb = utils.book_new(); + utils.book_append_sheet(wb, ws, "SheetJS"); + + return wb; +} +``` + +### Displaying Data + +The demos uses `react-native-table-component` to display the first worksheet. + +The demos use components similar to the example below: + +```jsx +import { ScrollView } from 'react-native'; +import { Table, Row, Rows, TableWrapper } from 'react-native-table-component'; + +( + {/* Horizontal scroll */} + + {/* Table container */} + + {/* Frozen Header Row */} + + {/* First row */} + + + {/* Scrollable Data Rows */} + + + {/* Remaining Rows */} + + + +
+
+) +``` + +`data.slice(1)` in the `Rows` component returns data starting from the second +row. This neatly skips the first header row. + ## Fetching Remote Data -:::info +React Native versions starting from `0.72.0`[^5] support binary data with `fetch`. -React Native `0.72.0` will support binary data with `fetch`. For older versions, -[a native library](#native-libraries) can provide support. - -::: - - -React Native 0.72.0 will support binary data with `fetch`: +This snippet downloads and parses : ```js /* fetch data into an ArrayBuffer */ -const ab = await (await fetch("https://sheetjs.com/pres.numbers")).arrayBuffer(); +const ab = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer(); /* parse data */ const wb = XLSX.read(ab); ``` ### Fetch Demo -The following demo uses `react-native-table-component` to display the first -worksheet in a simple table. - :::note -This demo was tested on an Intel Mac on 2023 April 24 with RN `0.72.0-rc.1`. +This demo was tested on an Intel Mac on 2023 July 02 with RN `0.72.1`. The iOS simulator runs iOS 16.2 on an iPhone SE (3rd generation). +The Android simulator runs Android 12.0 (S) API 31 on a Pixel 3. + ::: 1) Create project: ```bash -npx react-native init SheetJSRNFetch --version="0.72.0-rc.1" +npx -y react-native@0.72.1 init SheetJSRNFetch --version="0.72.1" ``` 2) Install shared dependencies: @@ -95,69 +238,46 @@ npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz npm i -S react-native-table-component@1.2.0 @types/react-native-table-component`} -Refresh iOS project by running `pod install` from the `ios` subfolder: - -```bash -cd ios; pod install; cd .. -``` - 3) Download [`App.tsx`](pathname:///reactnative/App.tsx) and replace: ```bash curl -LO https://docs.sheetjs.com/reactnative/App.tsx ``` -**iOS Testing** - -Start the iOS emulator: - -```bash -npx react-native run-ios -``` - -When opened, the app should look like the "Before" screenshot below. After -tapping "Import data from a spreadsheet", the app should show new data: - - - - -
BeforeAfter
- -![before screenshot](pathname:///reactnative/iosfetch1.png) - - - -![after screenshot](pathname:///reactnative/iosfetch2.png) - -
- **Android Testing** -Start the Android emulator: +4) Install or switch to Java 11[^6] + +:::note pass + +When the demo was last tested on macOS, `java -version` displayed the following: + +``` +openjdk version "11.0.19" 2023-04-18 LTS +OpenJDK Runtime Environment Zulu11.64+19-CA (build 11.0.19+7-LTS) +OpenJDK 64-Bit Server VM Zulu11.64+19-CA (build 11.0.19+7-LTS, mixed mode) +``` + +::: + +5) Start the Android emulator: ```bash npx react-native run-android ``` -:::note +:::caution pass -When this demo was last tested, the simulator failed with the message +If the initial launch fails with an error referencing the emulator, manually +start the emulator and try again. -> Unable to load script. Make sure you're either Running Metro ... - -The workaround is to launch Metro directly: - -```bash -npx react-native start -``` - -Press `a` in the terminal window and Metro will try to reload the app. +Gradle errors typically stem from a Java version mismatch. Run `java -version` +and verify that the Java major version is 11. ::: - -When opened, the app should look like the "Before" screenshot below. After -tapping "Import data from a spreadsheet", the app should show new data: +6) When opened, the app should look like the "Before" screenshot below. After +tapping "Import data from a spreadsheet", verify that the app shows new data: @@ -172,10 +292,45 @@ tapping "Import data from a spreadsheet", the app should show new data:
Before
+**iOS Testing** -## Native Libraries +:::warning pass -:::warning +iOS testing requires macOS. It does not work on Windows. + +::: + +7) Refresh iOS project by running `pod install` from the `ios` subfolder: + +```bash +cd ios; pod install; cd .. +``` + +8) Start the iOS emulator: + +```bash +npx react-native run-ios +``` + +9) When opened, the app should look like the "Before" screenshot below. After +tapping "Import data from a spreadsheet", verify that the app shows new data: + + + + +
BeforeAfter
+ +![before screenshot](pathname:///reactnative/iosfetch1.png) + + + +![after screenshot](pathname:///reactnative/iosfetch2.png) + +
+ +## Local Files + +:::warning pass 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. @@ -187,40 +342,41 @@ 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). +("A" for Android and "I" for iOS). -| 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 | +| File system Plugin | File Picker Plugin | OS | +|:---------------------------|:-------------------------------|:----:| +| `react-native-file-access` | `react-native-document-picker` | `AI` | +| `react-native-blob-util` | `react-native-document-picker` | `AI` | +| `rn-fetch-blob` | `react-native-document-picker` | `AI` | +| `react-native-fs` | `react-native-document-picker` | `AI` | +| `expo-file-system` | `expo-document-picker` | ` I` | ### RN File Picker +The "File Picker" library handles two platform-specific steps: + +1) Show a view that allows users to select a file from their device + +2) Copy the selected file to a location that can be read by the application + The following libraries have been tested: #### `react-native-document-picker`
Selecting a file (click to show) -When a copy is not needed: +The setting `copyTo: "cachesDirectory"` must be set: ```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 f = await pickSingle({ + allowMultiSelection: false, + // highlight-next-line + copyTo: "cachesDirectory", + mode: "open" +}); const path = f.fileCopyUri; // this path can be read by RN file plugins ``` @@ -240,7 +396,7 @@ const result = await DocumentPicker.getDocumentAsync({ copyToCacheDirectory: true, type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] }); -const path = result.uri; +const path = result.uri; // this path can be read by RN file plugins ```
@@ -433,11 +589,11 @@ await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx -## Demo +### Demo :::note -This demo was tested on an Intel Mac on 2023 April 30 with RN `0.72.0-rc.1`. +This demo was tested on an Intel Mac on 2023 July 02 with RN `0.72.1`. The iOS simulator runs iOS 16.2 on an iPhone 14. @@ -449,8 +605,8 @@ The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5. 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. +before approaching this demo.[^7] Details including Android Virtual Device +configuration are not covered here. ::: @@ -459,7 +615,7 @@ This example tries to separate the library-specific functions. 1) Create project: ```bash -npx react-native init SheetJSRN --version="0.72.0-rc.1" +npx react-native init SheetJSRN --version="0.72.1" ``` 2) Install shared dependencies: @@ -519,7 +675,6 @@ 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 + "/"); @@ -560,11 +715,6 @@ import { Platform } from 'react-native'; import { Dirs, FileSystem } from 'react-native-file-access'; async function pickAndParse() { - /* react-native-file-access in RN < 0.72 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'}); - /* react-native-file-access in RN >= 0.72 needs a copy */ const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" }); let path = f.fileCopyUri; const res = await (await fetch(path)).arrayBuffer(); @@ -603,7 +753,6 @@ 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 + "/"); @@ -643,7 +792,6 @@ 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'}); @@ -665,12 +813,7 @@ const make_width = ws => { :::warning -At the time of testing, Expo did not support RN 0.72 . The project should be -created with React Native 0.67.2: - -```bash -npx react-native init SheetJSRN --version="0.67.2" -``` +At the time of testing, Expo Modules were incompatible with Android projects. ::: @@ -793,3 +936,11 @@ Once selected, the screen should refresh with new contents: adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx npx xlsx-cli /tmp/sheetjsw.xlsx ``` + +[^1]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) and select the appropriate "Development OS". +[^2]: See ["Array of Arrays" in the API reference](/docs/api/utilities/array#array-of-arrays) +[^3]: See ["Array Output" in "Utility Functions"](/docs/api/utilities/array#array-output) +[^4]: See ["Array of Arrays Input" in "Utility Functions"](/docs/api/utilities/array#array-of-arrays-input) +[^5]: React-Native commit [`5b597b5`](https://github.com/facebook/react-native/commit/5b597b5ff94953accc635ed3090186baeecb3873) added the final piece required for `fetch` support. It landed in version `0.72.0-rc.1` and is available in official releases starting from `0.72.0`. +[^6]: When the demo was last tested, the Zulu11 distribution of Java 11 was installed through the macOS Brew package manager. [Direct downloads are available at `azul.com`](https://www.azul.com/downloads/?version=java-11-lts&package=jdk#zulu) +[^7]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) for Android (and iOS, if applicable) \ No newline at end of file