2023-01-05 03:57:48 +00:00
|
|
|
|
---
|
2023-02-13 04:07:25 +00:00
|
|
|
|
title: React Native
|
2023-01-05 23:33:49 +00:00
|
|
|
|
pagination_prev: demos/mobile/index
|
2023-02-28 11:40:44 +00:00
|
|
|
|
pagination_next: demos/data/index
|
2023-01-05 03:57:48 +00:00
|
|
|
|
sidebar_position: 6
|
|
|
|
|
sidebar_custom_props:
|
|
|
|
|
summary: Native Components with React
|
|
|
|
|
---
|
|
|
|
|
|
2023-04-27 09:12:19 +00:00
|
|
|
|
import current from '/version.js';
|
2023-01-05 03:57:48 +00:00
|
|
|
|
import Tabs from '@theme/Tabs';
|
|
|
|
|
import TabItem from '@theme/TabItem';
|
2023-05-07 13:58:36 +00:00
|
|
|
|
import CodeBlock from '@theme/CodeBlock';
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
|
|
This section covers React Native for desktop applications. For iOS and Android
|
2023-01-05 23:33:49 +00:00
|
|
|
|
applications, [check the mobile demo](/docs/demos/mobile/reactnative)
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
React Native for Windows + macOS is a backend for React Native that supports
|
|
|
|
|
native apps. The Windows backend builds apps for use on Windows 10 / 11, Xbox,
|
|
|
|
|
and other supported platforms. The macOS backend supports macOS 10.14 SDK
|
|
|
|
|
|
|
|
|
|
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
|
|
|
|
|
from the main app script. File operations must be written in native code.
|
|
|
|
|
|
|
|
|
|
The "Complete Example" creates an app that looks like the screenshots below:
|
|
|
|
|
|
|
|
|
|
<table><thead><tr>
|
2023-02-21 08:40:30 +00:00
|
|
|
|
<th><a href="#windows-demo">Win10</a></th>
|
2023-01-05 03:57:48 +00:00
|
|
|
|
<th><a href="#macos-demo">macOS</a></th>
|
|
|
|
|
</tr></thead><tbody><tr><td>
|
|
|
|
|
|
|
|
|
|
![Windows screenshot](pathname:///reactnative/rnw.png)
|
|
|
|
|
|
|
|
|
|
</td><td>
|
|
|
|
|
|
|
|
|
|
![macOS screenshot](pathname:///reactnative/rnm.png)
|
|
|
|
|
|
|
|
|
|
</td></tr></tbody></table>
|
|
|
|
|
|
|
|
|
|
## Native Modules
|
|
|
|
|
|
|
|
|
|
:::caution
|
|
|
|
|
|
|
|
|
|
As with the mobile versions of React Native, file operations are not provided
|
|
|
|
|
by the base SDK. The examples include native code for both Windows and macOS.
|
|
|
|
|
|
|
|
|
|
The Windows demo assumes some familiarity with C++ / C# and the macOS demo
|
|
|
|
|
assumes some familiarity with Objective-C.
|
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
React Native for Windows + macOS use [Turbo Modules](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules)
|
|
|
|
|
for effortless integration with native libraries and code.
|
|
|
|
|
|
|
|
|
|
The demos define a native module named `DocumentPicker`.
|
|
|
|
|
|
|
|
|
|
### Reading Files
|
|
|
|
|
|
|
|
|
|
The native modules in the demos define a `PickAndRead` function that will show
|
|
|
|
|
the file picker, read the file contents, and return a Base64 string.
|
|
|
|
|
|
|
|
|
|
Only the main UI thread can show file pickers. This is similar to Web Worker
|
|
|
|
|
DOM access limitations in the Web platform.
|
|
|
|
|
|
|
|
|
|
_Integration_
|
|
|
|
|
|
|
|
|
|
This module can be referenced from the Turbo Module Registry:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
import { read } from 'xlsx';
|
|
|
|
|
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
|
|
|
|
|
const DocumentPicker = getEnforcing('DocumentPicker');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ... in some event handler ... */
|
|
|
|
|
async() => {
|
|
|
|
|
const b64 = await DocumentPicker.PickAndRead();
|
|
|
|
|
const wb = read(b64);
|
|
|
|
|
// DO SOMETHING WITH `wb` HERE
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
_Native Module_
|
|
|
|
|
|
|
|
|
|
<Tabs groupId="os">
|
|
|
|
|
<TabItem value="win" label="Windows">
|
|
|
|
|
|
|
|
|
|
React Native Windows supports C++ and C# projects.
|
|
|
|
|
|
|
|
|
|
<Tabs groupId="rnwlang">
|
|
|
|
|
<TabItem value="cs" label="C#">
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
[ReactMethod("PickAndRead")]
|
|
|
|
|
public async void PickAndRead(IReactPromise<string> result) {
|
|
|
|
|
/* perform file picker action in the UI thread */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
context.Handle.UIDispatcher.Post(async() => { try {
|
|
|
|
|
/* create file picker */
|
|
|
|
|
var picker = new FileOpenPicker();
|
|
|
|
|
picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
|
|
|
|
|
picker.FileTypeFilter.Add(".xlsx");
|
|
|
|
|
picker.FileTypeFilter.Add(".xls");
|
|
|
|
|
|
|
|
|
|
/* show file picker */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
var file = await picker.PickSingleFileAsync();
|
|
|
|
|
if(file == null) throw new Exception("File not found");
|
|
|
|
|
|
|
|
|
|
/* read data and return base64 string */
|
|
|
|
|
var buf = await FileIO.ReadBufferAsync(file);
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
|
|
|
|
|
} catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
<TabItem value="cpp" label="C++">
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
REACT_METHOD(PickAndRead);
|
|
|
|
|
void PickAndRead(ReactPromise<winrt::hstring> promise) noexcept {
|
|
|
|
|
auto prom = promise;
|
|
|
|
|
/* perform file picker action in the UI thread */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget {
|
|
|
|
|
auto p = prom; // promise -> prom -> p dance avoids promise destruction
|
|
|
|
|
|
|
|
|
|
/* create file picker */
|
|
|
|
|
FileOpenPicker picker;
|
|
|
|
|
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
|
|
|
|
|
picker.FileTypeFilter().Append(L".xlsx");
|
|
|
|
|
picker.FileTypeFilter().Append(L".xls");
|
|
|
|
|
|
|
|
|
|
/* show file picker */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
StorageFile file = co_await picker.PickSingleFileAsync();
|
|
|
|
|
if(file == nullptr) { p.Reject("File not Found"); co_return; }
|
|
|
|
|
|
|
|
|
|
/* read data and return base64 string */
|
|
|
|
|
auto buf = co_await FileIO::ReadBufferAsync(file);
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
p.Resolve(CryptographicBuffer::EncodeToBase64String(buf));
|
|
|
|
|
co_return;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
<TabItem value="mac" label="macOS">
|
|
|
|
|
|
|
|
|
|
React Native macOS supports Objective-C modules
|
|
|
|
|
|
|
|
|
|
```objc
|
|
|
|
|
/* the resolve/reject is projected on the JS side as a Promise */
|
|
|
|
|
RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
|
|
|
/* perform file picker action in the UI thread */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
RCTExecuteOnMainQueue(^{
|
|
|
|
|
/* create file picker */
|
|
|
|
|
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
|
|
|
|
[panel setCanChooseDirectories:NO];
|
|
|
|
|
[panel setAllowsMultipleSelection:NO];
|
|
|
|
|
[panel setMessage:@"Select a spreadsheet to read"];
|
|
|
|
|
|
|
|
|
|
/* show file picker */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
[panel beginWithCompletionHandler:^(NSInteger result){
|
|
|
|
|
if (result == NSModalResponseOK) {
|
|
|
|
|
/* read data and return base64 string */
|
|
|
|
|
NSURL *selected = [[panel URLs] objectAtIndex:0];
|
|
|
|
|
NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil];
|
|
|
|
|
if(hFile) {
|
|
|
|
|
NSData *data = [hFile readDataToEndOfFile];
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
resolve([data base64EncodedStringWithOptions:0]);
|
|
|
|
|
} else reject(@"read_failure", @"Could not read selected file!", nil);
|
|
|
|
|
} else reject(@"select_failure", @"No file selected!", nil);
|
|
|
|
|
}];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Windows Demo
|
|
|
|
|
|
|
|
|
|
This demo was tested against `v0.70.10` on 2023 January 04 in Windows 10.
|
|
|
|
|
|
|
|
|
|
:::warning
|
|
|
|
|
|
|
|
|
|
There is no simple standalone executable file at the end of the process.
|
|
|
|
|
|
|
|
|
|
[The official documentation describes distribution strategies](https://microsoft.github.io/react-native-windows/docs/native-code#distribution)
|
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
|
|
React Native Windows supports writing native code in C++ or C#. This demo has
|
|
|
|
|
been tested against both application types.
|
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
0) Follow the ["Getting Started" guide](https://microsoft.github.io/react-native-windows/docs/getting-started)
|
|
|
|
|
|
|
|
|
|
:::caution
|
|
|
|
|
|
2023-01-20 02:25:16 +00:00
|
|
|
|
At the time of testing, NodeJS `v16` was required. A tool like
|
|
|
|
|
[`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) should be
|
|
|
|
|
used to switch the NodeJS version.
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
1) Create a new project using React Native `0.70`:
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
npx react-native init SheetJSWin --template react-native@^0.70.0
|
|
|
|
|
cd .\SheetJSWin\
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Create the Windows part of the application:
|
|
|
|
|
|
|
|
|
|
<Tabs groupId="rnwlang">
|
|
|
|
|
<TabItem value="cs" label="C#">
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
npx react-native-windows-init --no-telemetry --overwrite --language=cs
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
<TabItem value="cpp" label="C++">
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
npx react-native-windows-init --no-telemetry --overwrite
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
2023-04-27 09:12:19 +00:00
|
|
|
|
Install the SheetJS library:
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
2023-05-07 13:58:36 +00:00
|
|
|
|
<CodeBlock language="bash">{`\
|
2023-04-27 09:12:19 +00:00
|
|
|
|
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
|
2023-05-07 13:58:36 +00:00
|
|
|
|
</CodeBlock>
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
|
|
|
|
To ensure that the app works, launch the app:
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
npx react-native run-windows --no-telemetry
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<Tabs groupId="rnwlang">
|
|
|
|
|
<TabItem value="cs" label="C#">
|
|
|
|
|
|
|
|
|
|
2) Create the file `windows\SheetJSWin\DocumentPicker.cs` with the following:
|
|
|
|
|
|
|
|
|
|
```csharp title="windows\SheetJSWin\DocumentPicker.cs"
|
|
|
|
|
using System;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Windows.Security.Cryptography;
|
|
|
|
|
using Windows.Storage;
|
|
|
|
|
using Windows.Storage.Pickers;
|
|
|
|
|
using Microsoft.ReactNative.Managed;
|
|
|
|
|
|
|
|
|
|
namespace SheetJSWin {
|
|
|
|
|
[ReactModule]
|
|
|
|
|
class DocumentPicker {
|
|
|
|
|
private ReactContext context;
|
|
|
|
|
[ReactInitializer]
|
|
|
|
|
public void Initialize(ReactContext reactContext) { context = reactContext; }
|
|
|
|
|
|
|
|
|
|
[ReactMethod("PickAndRead")]
|
|
|
|
|
public async void PickAndRead(IReactPromise<string> result) {
|
|
|
|
|
context.Handle.UIDispatcher.Post(async() => { try {
|
|
|
|
|
var picker = new FileOpenPicker();
|
|
|
|
|
picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
|
|
|
|
|
picker.FileTypeFilter.Add(".xlsx");
|
|
|
|
|
picker.FileTypeFilter.Add(".xls");
|
|
|
|
|
|
|
|
|
|
var file = await picker.PickSingleFileAsync();
|
|
|
|
|
if(file == null) throw new Exception("File not found");
|
|
|
|
|
|
|
|
|
|
var buf = await FileIO.ReadBufferAsync(file);
|
|
|
|
|
result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
|
|
|
|
|
} catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3) Add the highlighted line to `windows\SheetJSWin\SheetJSWin.csproj`. Look for
|
|
|
|
|
the `ItemGroup` that contains `ReactPackageProvider.cs`:
|
|
|
|
|
|
|
|
|
|
```xml title="windows\SheetJSWin\SheetJSWin.csproj"
|
|
|
|
|
<!-- highlight-next-line -->
|
|
|
|
|
<Compile Include="DocumentPicker.cs" />
|
|
|
|
|
<Compile Include="ReactPackageProvider.cs" />
|
|
|
|
|
</ItemGroup>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
<TabItem value="cpp" label="C++">
|
|
|
|
|
|
|
|
|
|
2) Create the file `windows\SheetJSWin\DocumentPicker.h` with the following:
|
|
|
|
|
|
|
|
|
|
```cpp title="windows\SheetJSWin\DocumentPicker.h"
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include <winrt/Windows.Storage.Pickers.h>
|
|
|
|
|
#include <winrt/Windows.Security.Cryptography.h>
|
|
|
|
|
#include "NativeModules.h"
|
|
|
|
|
|
|
|
|
|
using namespace winrt::Microsoft::ReactNative;
|
|
|
|
|
using namespace winrt::Windows::Storage;
|
|
|
|
|
using namespace winrt::Windows::Storage::Pickers;
|
|
|
|
|
using namespace winrt::Windows::Security::Cryptography;
|
|
|
|
|
|
|
|
|
|
namespace SheetJSWin {
|
|
|
|
|
REACT_MODULE(DocumentPicker);
|
|
|
|
|
struct DocumentPicker {
|
|
|
|
|
REACT_INIT(Initialize);
|
|
|
|
|
void Initialize(const ReactContext& reactContext) noexcept {
|
|
|
|
|
context = reactContext;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
REACT_METHOD(PickAndRead);
|
|
|
|
|
void PickAndRead(ReactPromise<winrt::hstring> promise) noexcept {
|
|
|
|
|
auto prom = promise;
|
|
|
|
|
context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget {
|
|
|
|
|
auto p = prom;
|
|
|
|
|
FileOpenPicker picker;
|
|
|
|
|
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
|
|
|
|
|
picker.FileTypeFilter().Append(L".xlsx");
|
|
|
|
|
picker.FileTypeFilter().Append(L".xls");
|
|
|
|
|
|
|
|
|
|
StorageFile file = co_await picker.PickSingleFileAsync();
|
|
|
|
|
if(file == nullptr) { p.Reject("File not Found"); co_return; }
|
|
|
|
|
|
|
|
|
|
auto buf = co_await FileIO::ReadBufferAsync(file);
|
|
|
|
|
p.Resolve(CryptographicBuffer::EncodeToBase64String(buf));
|
|
|
|
|
co_return;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
ReactContext context{nullptr};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3) Add the highlighted line to `windows\SheetJSWin\ReactPackageProvider.cpp`:
|
|
|
|
|
|
|
|
|
|
```cpp title="windows\SheetJSWin\ReactPackageProvider.cpp"
|
|
|
|
|
#include "ReactPackageProvider.h"
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
#include "DocumentPicker.h"
|
|
|
|
|
#include "NativeModules.h"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</TabItem>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
Now the native module will be added to the app.
|
|
|
|
|
|
|
|
|
|
4) Remove `App.js` and save the following to `App.tsx`:
|
|
|
|
|
|
|
|
|
|
```tsx title="App.tsx"
|
|
|
|
|
import React, { useState, type Node } from 'react';
|
|
|
|
|
import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
|
|
|
|
|
import { read, utils, version } from 'xlsx';
|
|
|
|
|
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
|
|
|
|
|
const DocumentPicker = getEnforcing('DocumentPicker');
|
|
|
|
|
|
|
|
|
|
const App: () => Node = () => {
|
|
|
|
|
|
|
|
|
|
const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SafeAreaView style={styles.outer}>
|
|
|
|
|
<Text style={styles.title}>SheetJS × React Native Windows {version}</Text>
|
|
|
|
|
<TouchableHighlight onPress={async() => {
|
|
|
|
|
try {
|
|
|
|
|
const b64 = await DocumentPicker.PickAndRead();
|
|
|
|
|
const wb = read(b64);
|
|
|
|
|
setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } ));
|
|
|
|
|
} catch(err) { alert(`Error: ${err.message}`); }
|
|
|
|
|
}}><Text style={styles.button}>Click here to Open File!</Text></TouchableHighlight>
|
|
|
|
|
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
|
|
|
|
<View style={styles.table}>{aoa.map((row,R) => (
|
|
|
|
|
<View style={styles.row} key={R}>{row.map((cell,C) => (
|
|
|
|
|
<View style={styles.cell} key={C}><Text>{cell}</Text></View>
|
|
|
|
|
))}</View>
|
|
|
|
|
))}</View>
|
|
|
|
|
</ScrollView>
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
cell: { flex: 4 },
|
|
|
|
|
row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', },
|
|
|
|
|
table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', },
|
|
|
|
|
outer: { marginTop: 32, paddingHorizontal: 24, },
|
|
|
|
|
title: { fontSize: 24, fontWeight: '600', },
|
|
|
|
|
button: { marginTop: 8, fontSize: 18, fontWeight: '400', },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default App;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
5) Test the app again:
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
npx react-native run-windows --no-telemetry
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Download <https://sheetjs.com/pres.xlsx>, then click on "open file". Use the
|
|
|
|
|
file picker to select the `pres.xlsx` file and the app will show the data.
|
|
|
|
|
|
|
|
|
|
## macOS Demo
|
|
|
|
|
|
|
|
|
|
This demo was tested against `v0.64.30` on 2023 January 04 in MacOS 12.4
|
|
|
|
|
|
|
|
|
|
0) Follow the [React Native](https://reactnative.dev/docs/environment-setup)
|
|
|
|
|
guide for React Native CLI on MacOS.
|
|
|
|
|
|
|
|
|
|
:::caution
|
|
|
|
|
|
2023-01-20 02:25:16 +00:00
|
|
|
|
At the time of testing, NodeJS `v16` was required. A tool like
|
|
|
|
|
[`n`](https://github.com/tj/n/) should be used to switch the NodeJS version.
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
1) Create a new project using React Native `0.64`:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npx react-native init SheetJSmacOS --template react-native@^0.64.0
|
|
|
|
|
cd SheetJSmacOS
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Create the MacOS part of the application:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npx react-native-macos-init --no-telemetry
|
|
|
|
|
```
|
|
|
|
|
|
2023-04-27 09:12:19 +00:00
|
|
|
|
Install the SheetJS library:
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
2023-05-07 13:58:36 +00:00
|
|
|
|
<CodeBlock language="bash">{`\
|
2023-04-27 09:12:19 +00:00
|
|
|
|
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
|
2023-05-07 13:58:36 +00:00
|
|
|
|
</CodeBlock>
|
2023-01-05 03:57:48 +00:00
|
|
|
|
|
|
|
|
|
To ensure that the app works, launch the app:
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
npx react-native run-macos
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Close the running app from the dock and close the Metro terminal window.
|
|
|
|
|
|
|
|
|
|
2) Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.h`:
|
|
|
|
|
|
|
|
|
|
```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.h"
|
|
|
|
|
#import <React/RCTBridgeModule.h>
|
|
|
|
|
@interface RCTDocumentPicker : NSObject <RCTBridgeModule>
|
|
|
|
|
@end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.m`:
|
|
|
|
|
|
|
|
|
|
```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.m"
|
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
|
#import <React/RCTUtils.h>
|
|
|
|
|
|
|
|
|
|
#import "RCTDocumentPicker.h"
|
|
|
|
|
|
|
|
|
|
@implementation RCTDocumentPicker
|
|
|
|
|
|
|
|
|
|
RCT_EXPORT_MODULE();
|
|
|
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
|
|
|
{
|
|
|
|
|
RCTExecuteOnMainQueue(^{
|
|
|
|
|
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
|
|
|
|
[panel setCanChooseDirectories:NO];
|
|
|
|
|
[panel setAllowsMultipleSelection:NO];
|
|
|
|
|
[panel setMessage:@"Select a spreadsheet to read"];
|
|
|
|
|
|
|
|
|
|
[panel beginWithCompletionHandler:^(NSInteger result){
|
|
|
|
|
if (result == NSModalResponseOK) {
|
|
|
|
|
NSURL *selected = [[panel URLs] objectAtIndex:0];
|
|
|
|
|
NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil];
|
|
|
|
|
if(hFile) {
|
|
|
|
|
NSData *data = [hFile readDataToEndOfFile];
|
|
|
|
|
resolve([data base64EncodedStringWithOptions:0]);
|
|
|
|
|
} else reject(@"read_failure", @"Could not read selected file!", nil);
|
|
|
|
|
} else reject(@"select_failure", @"No file selected!", nil);
|
|
|
|
|
}];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3) Edit the project file `macos/SheetJSmacOS.xcodeproj/project.pbxproj`.
|
|
|
|
|
|
|
|
|
|
There are four places where lines must be added:
|
|
|
|
|
|
|
|
|
|
A) Immediately after `/* Begin PBXBuildFile section */`
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
/* Begin PBXBuildFile section */
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */; };
|
|
|
|
|
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
B) Immediately after `/* Begin PBXFileReference section */`
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
/* Begin PBXFileReference section */
|
|
|
|
|
// highlight-start
|
|
|
|
|
4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTDocumentPicker.h; path = "SheetJSMacOS-macOS/RCTDocumentPicker.h"; sourceTree = "<group>"; };
|
|
|
|
|
4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTDocumentPicker.m; path = "SheetJSMacOS-macOS/RCTDocumentPicker.m"; sourceTree = "<group>"; };
|
|
|
|
|
// highlight-end
|
|
|
|
|
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
C) The goal is to add a reference to the `PBXSourcesBuildPhase` block for the
|
|
|
|
|
`macOS` target. To determine this, look in the `PBXNativeTarget section` for
|
|
|
|
|
a block with the comment `SheetJSmacOS-macOS`:
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
/* Begin PBXNativeTarget section */
|
|
|
|
|
...
|
|
|
|
|
productType = "com.apple.product-type.application";
|
|
|
|
|
};
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = {
|
|
|
|
|
isa = PBXNativeTarget;
|
|
|
|
|
...
|
|
|
|
|
/* End PBXNativeTarget section */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Within the block, look for `buildPhases` and find the hex string for `Sources`:
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
buildPhases = (
|
|
|
|
|
1A938104A937498D81B3BD3B /* [CP] Check Pods Manifest.lock */,
|
|
|
|
|
381D8A6F24576A6C00465D17 /* Start Packager */,
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
514201452437B4B30078DB4F /* Sources */,
|
|
|
|
|
514201462437B4B30078DB4F /* Frameworks */,
|
|
|
|
|
514201472437B4B30078DB4F /* Resources */,
|
|
|
|
|
381D8A6E24576A4E00465D17 /* Bundle React Native code and images */,
|
|
|
|
|
3689826CA944E2EF44FCBC17 /* [CP] Copy Pods Resources */,
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Search for that hex string (`514201452437B4B30078DB4F` in our example) in the
|
|
|
|
|
file and it should show up in a `PBXSourcesBuildPhase` section. Within `files`,
|
|
|
|
|
add the highlighted line:
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
514201452437B4B30078DB4F /* Sources */ = {
|
|
|
|
|
isa = PBXSourcesBuildPhase;
|
|
|
|
|
buildActionMask = 2147483647;
|
|
|
|
|
files = (
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */,
|
|
|
|
|
514201502437B4B30078DB4F /* ViewController.m in Sources */,
|
|
|
|
|
514201582437B4B40078DB4F /* main.m in Sources */,
|
|
|
|
|
5142014D2437B4B30078DB4F /* AppDelegate.m in Sources */,
|
|
|
|
|
);
|
|
|
|
|
runOnlyForDeploymentPostprocessing = 0;
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
D) The goal is to add file references to the "main group". Search for
|
|
|
|
|
`/* Begin PBXProject section */` and there should be one Project object.
|
|
|
|
|
Within the project object, look for `mainGroup`:
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
/* Begin PBXProject section */
|
|
|
|
|
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
|
|
|
|
isa = PBXProject;
|
|
|
|
|
...
|
|
|
|
|
Base,
|
|
|
|
|
);
|
|
|
|
|
// highlight-next-line
|
|
|
|
|
mainGroup = 83CBB9F61A601CBA00E9B192;
|
|
|
|
|
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
|
|
|
|
...
|
|
|
|
|
/* End PBXProject section */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Search for that hex string (`83CBB9F61A601CBA00E9B192` in our example) in the
|
|
|
|
|
file and it should show up in a `PBXGroup` section. Within `children`, add the
|
|
|
|
|
highlighted lines:
|
|
|
|
|
|
|
|
|
|
```plist
|
|
|
|
|
83CBB9F61A601CBA00E9B192 = {
|
|
|
|
|
isa = PBXGroup;
|
|
|
|
|
children = (
|
|
|
|
|
// highlight-start
|
|
|
|
|
4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */,
|
|
|
|
|
4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */,
|
|
|
|
|
// highlight-end
|
|
|
|
|
5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */,
|
|
|
|
|
13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */,
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
4) Replace `App.js` with the following:
|
|
|
|
|
|
|
|
|
|
```tsx title="App.js"
|
|
|
|
|
import React, { useState, type Node } from 'react';
|
|
|
|
|
import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
|
|
|
|
|
import { read, utils, version } from 'xlsx';
|
|
|
|
|
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
|
|
|
|
|
const DocumentPicker = getEnforcing('DocumentPicker');
|
|
|
|
|
|
|
|
|
|
const App: () => Node = () => {
|
|
|
|
|
|
|
|
|
|
const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SafeAreaView style={styles.outer}>
|
|
|
|
|
<Text style={styles.title}>SheetJS × React Native MacOS {version}</Text>
|
|
|
|
|
<TouchableHighlight onPress={async() => {
|
|
|
|
|
try {
|
|
|
|
|
const b64 = await DocumentPicker.PickAndRead();
|
|
|
|
|
const wb = read(b64);
|
|
|
|
|
setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } ));
|
|
|
|
|
} catch(err) { alert(`Error: ${err.message}`); }
|
|
|
|
|
}}><Text style={styles.button}>Click here to Open File!</Text></TouchableHighlight>
|
|
|
|
|
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
|
|
|
|
<View style={styles.table}>{aoa.map((row,R) => (
|
|
|
|
|
<View style={styles.row} key={R}>{row.map((cell,C) => (
|
|
|
|
|
<View style={styles.cell} key={C}><Text>{cell}</Text></View>
|
|
|
|
|
))}</View>
|
|
|
|
|
))}</View>
|
|
|
|
|
</ScrollView>
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
cell: { flex: 4 },
|
|
|
|
|
row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', },
|
|
|
|
|
table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', },
|
|
|
|
|
outer: { marginTop: 32, paddingHorizontal: 24, },
|
|
|
|
|
title: { fontSize: 24, fontWeight: '600', },
|
|
|
|
|
button: { marginTop: 8, fontSize: 18, fontWeight: '400', },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default App;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
5) Test the app:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npx react-native run-macos
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Download <https://sheetjs.com/pres.xlsx>, then click on "open file". Use the
|
|
|
|
|
file picker to select the `pres.xlsx` file and the app will show the data.
|
|
|
|
|
|
|
|
|
|
6) Make a release build:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release
|
|
|
|
|
```
|