diff --git a/docz/docs/03-demos/17-mobile/07-lynx.md b/docz/docs/03-demos/17-mobile/07-lynx.md new file mode 100644 index 0000000..e33b0e3 --- /dev/null +++ b/docz/docs/03-demos/17-mobile/07-lynx.md @@ -0,0 +1,388 @@ +--- +title: Sheets at Native Speed with Lynx +sidebar_label: Lynx +description: Build data-intensive mobile apps with Lynx. 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: 7 +sidebar_custom_props: + summary: Lynx + Native Rendering +--- + +import current from '/version.js'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import CodeBlock from '@theme/CodeBlock'; + +export const r = {style: {color:"red"}}; +export const g = {style: {color:"green"}}; +export const y = {style: {color:"gold"}}; +export const gr = {style: {color:"gray"}}; + +[Lynx](https://lynxjs.org/) is a modern cross-platform framework. It builds iOS, Android +and Web apps that use JavaScript for describing layouts and events. + +[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing +data from spreadsheets. + +:::caution Lynx support is considered experimental. + +Lynx is a great, fast, open-source alternative to React Native. Any issues should be reported to the Lynx project for +further diagnosis. + +::: + +This demo uses [ReactLynx](https://lynxjs.org/react) and SheetJS to process and generate +spreadsheets. We'll explore how to load SheetJS in ReactLynx app in a few ways: + +- ["Fetching Remote Data"](#fetching-remote-data) uses the built-in `fetch` to download +and parse remote workbook files. + +The "Fetching Remote Data" example creates an app that looks like the screenshots below: + + + + +
iOSAndroid
+ +![iOS screenshot](pathname:///lynx/react_lynx_fetch_demo_ios_1.jpeg) + + + +![Android screenshot](pathname:///lynx/react_lynx_fetch_demo_android_1.png) + +
+ +:::caution pass +**Before testing this demo, follow the official React Lynx 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 +a sample app in the Android and the iOS (if applicable) simulators. + +::: + + +## Integration Detail + +The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be +imported from any component or script in the app. + +### 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 represents a row in the table. + +This demo also keeps track of the column widths as a single array of numbers. +The widths are used by the display component. + +```tsx +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; +} +``` + +### Displaying Data + +The demo uses Lynx builtin element `` and `` to display the first worksheet. + +The demo uses components similar to the example below: + +```tsx +{/* Table container */} + + {/* Map through each row in the data array */} + {data.map((row, rowIndex) => ( + + {/* Map through each cell in the current row */} + {Array.isArray(row) && row.map((cell, cellIndex) => ( + {/* Cell with dynamic width based on content */} + + {/* Display cell content as text */} + {String(cell)} + + ))} + + ))} + +``` + +## Fetching Remote Data + +This snippet downloads and parses https://docs.sheetjs.com/pres.xlsx: + +```tsx +/* fetch data into an ArrayBuffer */ +const ab = await (await fetch("https://docs.sheetjs.com/pres.xlsx")).arrayBuffer(); +/* parse data */ +const wb = XLSX.read(ab); +``` + +The `data.map()` approach allows direct rendering of the worksheet data in a tabular format, with each cell +width dynamically calculated based on content. + +### Fetch Demo + +:::note Tested Deployments + +This demo was tested in the following environments: + +**Simulators** + +| OS | Device | Lynx | LynxExplorer | Dev Platform | Date | +|:-----------|:--------------------|:---------|:-------------|:-------------|:-----------| +| Android 35 | Pixel 3a | `0.8.3` | `3.2.0-rc.0` | `darwin-arm` | 2025-03-11 | +| iOS 18.3 | iPhone 16 Pro | `0.8.3` | `3.2.0-rc.0` | `darwin-arm` | 2025-03-11 | + +::: + +:::danger Real Devices + +As of `2025-03-11`, there is no simple, standalone guide on how to build your Lynx app for real devices. + +::: + +:::caution + +First install Lynx by following the Guide![^1]. Make sure you can run a basic test app on your +simulator before continuing! + +::: + +0) Install Lynx dependencies + +1) Create project: + +```bash +pnpm create rspeedy@0.8.3 -d SheetJSLynxFetch -t react-ts --tools biome +``` + +2) Install shared dependencies: + +{`\ +cd SheetJSLynxFetch +curl -o ./src/assets/SheetJS-logo.png https://docs.sheetjs.com/logo.png +pnpm install +pnpm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`} + + +3) Download [`App.tsx`](pathname:///lynx/App.tsx) and replace: + +```bash +curl -o ./src/App.tsx https://docs.sheetjs.com/lynx/App.tsx +``` + +4) Download [`App.css`](pathname://lynx/App.css) and replace: + +```bash +curl -o ./src/App.css https://docs.sheetjs.com/lynx/App.css +``` + +5) Start the development server, run: + +```bash +pnpm run dev +``` + +6) Start the Android emulator: + +
+ Details (click to hide) + +**Android Studio** + +In Android Studio, click "More actions" > "Virtual Device Manager". Look for the +emulated device in the list and click the ▶ button to play. + +**Command Line** +List the available emulators with `emulator -list-avds` + +``` +shjs@sheetjs SheetJSLynxFetch % emulator -list-avds +Medium_Phone_API_35 +^^^^^^^^^^^^^^^^^^^--- emulator name +``` + +Emulator name should be passed to `emulator -avd`. In a previous test, the name was +`Medium_Phone_API_35` and the launch command was: + +```bash +emulator -avd Medium_Phone_API_35 +``` + +:::note pass + +On macOS, `~/Library/Android/sdk/emulator/` is the typical location +for the `emulator` binary. If it cannot be found, add the folder to `PATH`: + +```bash +export PATH="$PATH":~/Library/Android/sdk/emulator +emulator -avd Medium_Phone_API_35 +``` + +::: + +
+ +7) While the Android emulator is open, download LynxExplorer[^4] - (Is a sandbox for trying out Lynx quickly) + + + + 1. Download the pre-build app from the [GitHub Release](https://github.com/lynx-family/lynx/releases/tag/3.2.0-rc.0) and + select the APK `LynxExplorer-noasan-release.apk`. + 2. Drag and drop the APK `LynxExplorer-noasan-release.apk` in to your Android simulator. + + + +--- +8) From [step 5](#step5), you will see a QR code appear in the terminal with a hyperlink like this. Copy the HTTP link. + +![lynx live server link](pathname:///lynx/lynx_live_server_link.png) + +9) In the simulator, open the _LynxExplorer_ app. In the input field labeled **Enter Card URL**, paste the link and +click **Go**. + +10) 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:///lynx/react_lynx_fetch_demo_android_1.png) + + + +![after screenshot](pathname:///lynx/react_lynx_fetch_demo_android_2.png) + +
+ + +**iOS Testing** + +:::danger pass + +**iOS testing can only be performed on Apple hardware running macOS!** + +Xcode and iOS simulators are not available on Windows or Linux. + +::: + + + + 1. Install Xcode open up the Mac App Store, search for [Xcode](https://apps.apple.com/us/app/xcode/id497799835), and + click Install (or Update if you have it already). + 2. Download [`LynxExplorer-arm64.app.tar.gz`](https://github.com/lynx-family/lynx/releases/latest/download/LynxExplorer-arm64.app.tar.gz). + 3. Then, extract the downloaded archive: + + ```bash + mkdir -p LynxExplorer-arm64.app/ + tar -zxf LynxExplorer-arm64.app.tar.gz -C LynxExplorer-arm64.app/ + ``` + + 4. Install LynxExplorer[^4] on simulator open Xcode, and choose Open Developer Tool from the Xcode menu. Click the + Simulator to launch one. Drag "LynxExplorer-arm64.app" into it. + + + + +11) From [step 5](#step5), you will see a QR code appear in the terminal with a hyperlink like this. Copy the HTTP link. + +![lynx live server link](pathname:///lynx/lynx_live_server_link.png) + +12) In the simulator, open the _LynxExplorer_ app. In the input field labeled **Enter Card URL**, paste the link and + click **Go**. + +13) 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:///lynx/react_lynx_fetch_demo_ios_1.jpeg) + + + +![after screenshot](pathname:///lynx/react_lynx_fetch_demo_ios_2.jpeg) + +
+ +[^1]: Follow the ["Quick Start guide](https://lynxjs.org/guide/start/quick-start.html) and select the appropriate +"Lynx Explorer sandbox" +[^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 ["LynxExplorer sandbox"](https://github.com/lynx-family/lynx/releases/tag/3.2.0-rc.0/) \ No newline at end of file diff --git a/docz/static/lynx/App.css b/docz/static/lynx/App.css new file mode 100644 index 0000000..94efea5 --- /dev/null +++ b/docz/static/lynx/App.css @@ -0,0 +1,62 @@ +:root { + background-color: #fff; + --color-text: #000; +} + +.App { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 100px +} + +text { + color: var(--color-text); +} + +.Title { + font-size: 24px; + font-weight: 400; + justify-content: center; + align-items: center; + +} + +.Button { + margin-top: 12px; + background-color: #841584; + --color-text: #fff; + padding: 12px; + border-radius: 4px; + justify-content: center; + align-items: center; +} + +.Logo { + height: 24px; + width: 24px; +} + +.Table { + margin-top: 16px; + border: 1px solid #000; + border-radius: 4px; + overflow: hidden; +} + +.Row { + display: flex; + flex-direction: row; + border-bottom: 1px solid #000; +} + +.Cell { + padding: 8px; + border-right: 1px solid #000; + overflow: hidden; + text-overflow: ellipsis; + min-width: 40px; +} diff --git a/docz/static/lynx/App.tsx b/docz/static/lynx/App.tsx new file mode 100644 index 0000000..f23ad9f --- /dev/null +++ b/docz/static/lynx/App.tsx @@ -0,0 +1,69 @@ +/* sheetjs (C) SheetJS -- https://sheetjs.com */ + +import './App.css' +import { useCallback, useState } from '@lynx-js/react' +import SheetJSLogo from './assets/SheetJS-logo.png'; +import { read, utils, WorkSheet } from 'xlsx'; + +const make_width = (ws: WorkSheet): number[] => { + const aoa = utils.sheet_to_json(ws, { header: 1 }), res: number[] = []; + aoa.forEach((r: any) => { r.forEach((c: any, C: any) => { res[C] = Math.max(res[C] || 60, String(c).length * 10); }); }); + for (let C = 0; C < res.length; ++C) if (!res[C]) res[C] = 60; + return res; +}; + +export function App() { + 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)); + + const importFile = useCallback(async () => { + try { + const ab = await (await fetch("https://docs.sheetjs.com/pres.numbers")).arrayBuffer(); + const wb = read(ab); + + /* 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); + console.log(data); + + setWidths(make_width(ws)); + } catch (err) { + console.log("importFile Error", "Error " + ((err as any).message || err)); + } + }, []); + + return ( + + +   SheetJS × React Lynx + + + IMPORT DATA FROM A SPREADSHEET + + Current Data + + {data.map((row, rowIndex) => ( + + {Array.isArray(row) && row.map((cell, cellIndex) => ( + + {cell} + + ))} + + ))} + + + + ) +} diff --git a/docz/static/lynx/lynx_live_server_link.png b/docz/static/lynx/lynx_live_server_link.png new file mode 100644 index 0000000..64575e4 Binary files /dev/null and b/docz/static/lynx/lynx_live_server_link.png differ diff --git a/docz/static/lynx/react_lynx_fetch_demo_android_1.png b/docz/static/lynx/react_lynx_fetch_demo_android_1.png new file mode 100644 index 0000000..064e546 Binary files /dev/null and b/docz/static/lynx/react_lynx_fetch_demo_android_1.png differ diff --git a/docz/static/lynx/react_lynx_fetch_demo_android_2.png b/docz/static/lynx/react_lynx_fetch_demo_android_2.png new file mode 100644 index 0000000..e93b332 Binary files /dev/null and b/docz/static/lynx/react_lynx_fetch_demo_android_2.png differ diff --git a/docz/static/lynx/react_lynx_fetch_demo_ios_1.jpeg b/docz/static/lynx/react_lynx_fetch_demo_ios_1.jpeg new file mode 100644 index 0000000..ba24713 Binary files /dev/null and b/docz/static/lynx/react_lynx_fetch_demo_ios_1.jpeg differ diff --git a/docz/static/lynx/react_lynx_fetch_demo_ios_2.jpeg b/docz/static/lynx/react_lynx_fetch_demo_ios_2.jpeg new file mode 100644 index 0000000..c60cea8 Binary files /dev/null and b/docz/static/lynx/react_lynx_fetch_demo_ios_2.jpeg differ