added a full Lynx demo

This commit is contained in:
Asad Karimov 2025-03-11 23:37:40 -04:00
parent 28453f2273
commit 1e69f93451
8 changed files with 519 additions and 0 deletions

@ -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:
<table><thead><tr>
<th><a href="#demo">iOS</a></th>
<th><a href="#demo">Android</a></th>
</tr></thead><tbody><tr><td>
![iOS screenshot](pathname:///lynx/react_lynx_fetch_demo_ios_1.jpeg)
</td><td>
![Android screenshot](pathname:///lynx/react_lynx_fetch_demo_android_1.png)
</td></tr></tbody></table>
:::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.
<table>
<thead><tr><th>Spreadsheet</th><th>Array of Arrays</th></tr></thead>
<tbody><tr><td>
![`pres.xlsx` data](pathname:///pres.png)
</td><td>
```js
[
["Name", "Index"],
["Bill Clinton", 42],
["GeorgeW Bush", 43],
["Barack Obama", 44],
["Donald Trump", 45],
["Joseph Biden", 46]
]
```
</td></tr></tbody></table>
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<any[]>([
"SheetJS".split(""),
[5, 4, 3, 3, 7, 9, 5],
[8, 6, 7, 5, 3, 0, 9]
]);
const [widths, setWidths] = useState<number[]>(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 `<view/>` and `<text/>` to display the first worksheet.
The demo uses components similar to the example below:
```tsx
{/* Table container */}
<view className='Table'>
{/* Map through each row in the data array */}
{data.map((row, rowIndex) => (
<view key={`row-${rowIndex}`} className="Row">
{/* Map through each cell in the current row */}
{Array.isArray(row) && row.map((cell, cellIndex) => (
{/* Cell with dynamic width based on content */}
<view
key={`cell-${rowIndex}-${cellIndex}`} className="Cell"
style={{ width: `${widths[cellIndex]}px` }}>
{/* Display cell content as text */}
<text>{String(cell)}</text>
</view>
))}
</view>
))}
</view>
```
## 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:
<CodeBlock language="bash">{`\
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`}
</CodeBlock>
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
```
<a id="step5"></a>
5) Start the development server, run:
```bash
pnpm run dev
```
6) Start the Android emulator:
<details open>
<summary><b>Details</b> (click to hide)</summary>
**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
```
:::
</details>
7) While the Android emulator is open, download LynxExplorer[^4] - (Is a sandbox for trying out Lynx quickly)
<Tabs groupId="lang">
<TabItem name="Android" value="Android">
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.
</TabItem>
</Tabs>
---
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:
<table><thead><tr>
<th>Before</th>
<th>After</th>
</tr></thead><tbody><tr><td>
![before screenshot](pathname:///lynx/react_lynx_fetch_demo_android_1.png)
</td><td>
![after screenshot](pathname:///lynx/react_lynx_fetch_demo_android_2.png)
</td></tr></tbody></table>
**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.
:::
<Tabs groupId="lang">
<TabItem name="iOS Simulator" value="iOS Simulator">
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.
</TabItem>
</Tabs>
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:
<table><thead><tr>
<th>Before</th>
<th>After</th>
</tr></thead><tbody><tr><td>
![before screenshot](pathname:///lynx/react_lynx_fetch_demo_ios_1.jpeg)
</td><td>
![after screenshot](pathname:///lynx/react_lynx_fetch_demo_ios_2.jpeg)
</td></tr></tbody></table>
[^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/)

62
docz/static/lynx/App.css Normal file

@ -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;
}

69
docz/static/lynx/App.tsx Normal file

@ -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<any[]>([
"SheetJS".split(""),
[5, 4, 3, 3, 7, 9, 5],
[8, 6, 7, 5, 3, 0, 9]
]);
const [widths, setWidths] = useState<number[]>(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 (
<view className='App'>
<text className="Title">
<image className="Logo" src={SheetJSLogo} /> &nbsp; SheetJS × React Lynx
</text>
<view className="Button" bindtap={importFile}>
<text>IMPORT DATA FROM A SPREADSHEET</text>
</view>
<text style={{ margin: '8px', fontWeight: '600'}}>Current Data</text>
<view className='Table'>
{data.map((row, rowIndex) => (
<view key={`row-${rowIndex}`} className="Row">
{Array.isArray(row) && row.map((cell, cellIndex) => (
<view
key={`cell-${rowIndex}-${cellIndex}`}
className="Cell"
style={{ width: `${widths[cellIndex]}px` }}
>
<text>{cell}</text>
</view>
))}
</view>
))}
</view>
<view style={{ flex: 1 }}></view>
</view>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB