forked from sheetjs/docs.sheetjs.com
added a full Lynx demo
This commit is contained in:
parent
28453f2273
commit
1e69f93451
388
docz/docs/03-demos/17-mobile/07-lynx.md
Normal file
388
docz/docs/03-demos/17-mobile/07-lynx.md
Normal file
@ -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>
|
||||
|
||||

|
||||
|
||||
</td><td>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
|
||||

|
||||
|
||||
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>
|
||||
|
||||

|
||||
|
||||
</td><td>
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
|
||||

|
||||
|
||||
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>
|
||||
|
||||

|
||||
|
||||
</td><td>
|
||||
|
||||

|
||||
|
||||
</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
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
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} /> 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>
|
||||
)
|
||||
}
|
BIN
docz/static/lynx/lynx_live_server_link.png
Normal file
BIN
docz/static/lynx/lynx_live_server_link.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
docz/static/lynx/react_lynx_fetch_demo_android_1.png
Normal file
BIN
docz/static/lynx/react_lynx_fetch_demo_android_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
docz/static/lynx/react_lynx_fetch_demo_android_2.png
Normal file
BIN
docz/static/lynx/react_lynx_fetch_demo_android_2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
BIN
docz/static/lynx/react_lynx_fetch_demo_ios_1.jpeg
Normal file
BIN
docz/static/lynx/react_lynx_fetch_demo_ios_1.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
BIN
docz/static/lynx/react_lynx_fetch_demo_ios_2.jpeg
Normal file
BIN
docz/static/lynx/react_lynx_fetch_demo_ios_2.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
Loading…
Reference in New Issue
Block a user