Native Sheets in NativeScript
NativeScript is a mobile app framework. It builds iOS and Android apps that use JavaScript for describing layouts and events.
SheetJS is a JavaScript library for reading and writing data from spreadsheets.
This demo uses NativeScript and SheetJS to process and generate spreadsheets. We'll explore how to load SheetJS in a NativeScript app; parse and generate spreadsheets stored on the device; and fetch and parse remote files.
The "Complete Example" creates an app that looks like the screenshots below:
iOS | Android |
---|---|
The discussion covers the NativeScript + Angular integration. Familiarity with Angular and TypeScript is assumed.
This demo was tested in the following environments:
OS | Type | Device | NS | Date |
---|---|---|---|---|
Android 34 | Sim | Pixel 3a | 8.6.1 | 2023-12-04 |
iOS 17.0.1 | Sim | iPhone SE (3rd gen) | 8.6.1 | 2023-12-04 |
Android 29 | Real | NVIDIA Shield | 8.6.1 | 2023-12-04 |
iOS 15.1 | Real | iPad Pro | 8.6.1 | 2023-12-04 |
Before starting this demo, manually disable telemetry.
NativeScript 8.6.1 split the telemetry into two parts: "usage" and "error". Both must be disabled separately:
npx -p nativescript ns usage-reporting disable
npx -p nativescript ns error-reporting disable
To verify telemetry was disabled:
npx -p nativescript ns usage-reporting status
npx -p nativescript ns error-reporting status
Integration Details
The SheetJS NodeJS Module can be imported from any component or script in the app.
The @nativescript/core/file-system
package provides classes for file access.
The File
class does not support binary data, but the file access singleton
from @nativescript/core
does support reading and writing ArrayBuffer
.
Reading and writing data require a URL. The following snippet searches typical document folders for a specified filename:
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
function get_url_for_filename(filename: string): string {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
return path.normalize(target.path + "///" + filename);
}
Reading Local Files
getFileAccess().readBufferAsync
can read data into an ArrayBuffer
object.
The SheetJS read
method1 can parse this data into a workbook object.2
import { getFileAccess } from '@nativescript/core';
import { read } from 'xlsx';
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
/* get data */
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(url);
/* read workbook */
const wb = read(ab);
After parsing into a workbook, the sheet_to_json
3 method can generate row
data objects:
import { utils } from 'xlsx';
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* generate array of row objects */
const data = utils.sheet_to_json(ws);
Writing Local Files
The SheetJS write
method4 with the option type: "binary"
will generate
Uint8Array
objects. getFileAccess().writeBufferAsync
can write data from a
Uint8Array
object to the device.
iOS supports Uint8Array
directly but Android requires a true array of numbers:
import { getFileAccess } from '@nativescript/core';
import { write } from 'xlsx';
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
/* generate Uint8Array */
const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'binary' });
/* attempt to save Uint8Array to file */
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
A worksheet can be generated from an array of row objects with the SheetJS
json_to_sheet
method5. After generating an array, the book_new
and
book_append_sheet
methods6 can create the workbook.
Fetching Remote Files
getFile
from @nativescript/core/http
can download files. After storing the
file in a temporary folder, getFileAccess().readBufferAsync
can read the data
and the SheetJS read
method7 can parse the file:
import { knownFolders, path, getFileAccess } from '@nativescript/core'
import { getFile } from '@nativescript/core/http';
import { read } from 'xlsx';
/* generate temporary path for the new file */
const temp: string = path.join(knownFolders.temp().path, "pres.xlsx");
/* download file */
const file = await getFile("https://sheetjs.com/pres.xlsx", temp)
/* get data */
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(file.path);
/* read workbook */
const wb = read(ab);
Complete Example
Platform Configuration
0) Disable telemetry:
npx -p nativescript ns usage-reporting disable
npx -p nativescript ns error-reporting disable
1) Follow the official Environment Setup instructions8.
When the demo was last tested, the latest version of the Android API was 34.
NativeScript did not support that API level. The exact error message from
npx -p nativescript ns doctor ios
clearly stated supported versions:
(x is red, body text is yellow)
✖ No compatible version of the Android SDK Build-tools are installed on your system. You can install any version in the following range: '>=23 <=33'.
The SDK Platform Android 13.0 ("Tiramisu")
was compatible with NativeScript.
Until NativeScript properly supports API level 34, "Tiramisu" must be used.
This requires installing the following packages from Android Studio:
Android 13.0 ("Tiramisu")
API Level33
Android SDK Build-Tools
Version33.0.2
2) Test the local system configuration for Android development:
npx -p nativescript ns doctor android
In the last macOS test, the following output was displayed:
Expected output (click to hide)
✔ Getting environment information
No issues were detected.
✔ Your ANDROID_HOME environment variable is set and points to correct directory.
✔ Your adb from the Android SDK is correctly installed.
✔ The Android SDK is installed.
✔ A compatible Android SDK for compilation is found.
✔ Javac is installed and is configured properly.
✔ The Java Development Kit (JDK) is installed and is configured properly.
✔ Getting NativeScript components versions information...
✔ Component nativescript has 8.6.1 version and is up to date.
3) Test the local system configuration for iOS development (macOS only):
npx -p nativescript ns doctor ios
In the last macOS test, the following output was displayed:
Expected output (click to hide)
✔ Getting environment information
No issues were detected.
✔ Xcode is installed and is configured properly.
✔ xcodeproj is installed and is configured properly.
✔ CocoaPods are installed.
✔ CocoaPods update is not required.
✔ CocoaPods are configured properly.
✔ Your current CocoaPods version is newer than 1.0.0.
✔ Python installed and configured correctly.
✔ The Python 'six' package is found.
✔ Xcode version 15.0.1 satisfies minimum required version 10.
✔ Getting NativeScript components versions information...
✔ Component nativescript has 8.6.1 version and is up to date.
Base Project
4) Create a skeleton NativeScript + Angular app:
npx -p nativescript ns create SheetJSNS --ng
5) Launch the app in the android simulator to verify the app:
cd SheetJSNS
npx -p nativescript ns run android
(this may take a while)
Once the simulator launches and the test app is displayed, end the script by
selecting the terminal and entering the key sequence CTRL + C
If the emulator is not running, nativescript
may fail with the message:
Emulator start failed with: No emulator image available for device identifier 'undefined'.
6) From the project folder, install the library:
npm i --save https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz
Add SheetJS
The goal of this section is to display the SheetJS library version number.
7) Edit src/app/item/items.component.ts
so that the component imports the
SheetJS version string and adds it to a version
variable in the component:
import { version } from 'xlsx';
import { Component, OnInit } from '@angular/core'
// ...
export class ItemsComponent implements OnInit {
items: Array<Item>
version = `SheetJS - ${version}`;
constructor(private itemService: ItemService) {}
// ...
8) Edit the template src/app/item/items.component.html
to reference version
in the title of the action bar:
<ActionBar [title]="version"></ActionBar>
<GridLayout>
<!-- ... -->
9) Relaunch the app in the Android simulator:
npx -p nativescript ns run android
The title bar should show the version.
Local Files
10) Add the Import and Export buttons to the template:
<ActionBar [title]="version"></ActionBar>
<StackLayout>
<StackLayout orientation="horizontal">
<Button text="Import File" (tap)="import()" style="padding: 10px"></Button>
<Button text="Export File" (tap)="export()" style="padding: 10px"></Button>
</StackLayout>
<ListView [items]="items">
<!-- ... -->
</ListView>
</StackLayout>
11) Add the import
and export
methods in the component script:
import { version, utils, read, write } from 'xlsx';
import { Dialogs, getFileAccess } from '@nativescript/core';
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
import { Component, OnInit } from '@angular/core'
import { Item } from './item'
import { ItemService } from './item.service'
function get_url_for_filename(filename: string): string {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
return path.normalize(target.path + "///" + filename);
}
@Component({
selector: 'ns-items',
templateUrl: './items.component.html',
})
export class ItemsComponent implements OnInit {
items: Array<Item>
version: string = `SheetJS - ${version}`;
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.items = this.itemService.getItems()
}
/* Import button */
async import() {
}
/* Export button */
async export() {
}
}
12) Restart the app process. Two buttons should show up at the top:
13) Implement import and export by adding the highlighted lines:
/* Import button */
async import() {
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
try {
await Dialogs.alert(`Attempting to read from SheetJSNS.xls at ${url}`);
/* get data */
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(url);
/* read workbook */
const wb = read(ab);
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* update table */
this.items = utils.sheet_to_json<Item>(ws);
} catch(e) { await Dialogs.alert(e.message); }
}
/* Export button */
async export() {
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
try {
/* create worksheet from data */
const ws = utils.json_to_sheet(this.items);
/* create workbook from worksheet */
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Sheet1");
/* generate Uint8Array */
const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'buffer' });
/* attempt to save Uint8Array to file */
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
await Dialogs.alert(`Wrote to SheetJSNS.xls at ${url}`);
} catch(e) { await Dialogs.alert(e.message); }
}
Android
14) Launch the app in the Android Simulator:
npx -p nativescript ns run android
If the app does not automatically launch, manually open the SheetJSNS
app.
15) Tap "Export File". A dialog will print where the file was written. Typically
the URL is /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls
16) Pull the file from the simulator:
adb root
adb pull /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls SheetJSNS.xls
If the emulator cannot be rooted:
adb shell "run-as org.nativescript.SheetJSNS cat /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls" > SheetJSNS.xls
17) Open SheetJSNS.xls
with a spreadsheet editor.
After the header row, insert a row with cell A2 = 0, B2 = SheetJS, C2 = Library:
id | name | role
0 | SheetJS | Library
1 | Ter Stegen | Goalkeeper
3 | Piqué | Defender
...
18) Push the file back to the simulator:
adb push SheetJSNS.xls /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls
If the emulator cannot be rooted:
dd if=SheetJSNS.xls | adb shell "run-as org.nativescript.SheetJSNS dd of=/data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls"
19) Tap "Import File". A dialog will print the path of the file that was read. The first item in the list will change.
iOS
20) Launch the app in the iOS Simulator:
npx -p nativescript ns run ios
21) Tap "Export File". A dialog will print where the file was written.
22) Open the file with a spreadsheet editor.
After the header row, insert a row with cell A2 = 0, B2 = SheetJS, C2 = Library:
id | name | role
0 | SheetJS | Library
1 | Ter Stegen | Goalkeeper
3 | Piqué | Defender
...
23) Restart the app after saving the file.
24) Tap "Import File". A dialog will print the path of the file that was read. The first item in the list will change:
Fetching Files
25) In src/app/item/items.component.ts
, make ngOnInit
asynchronous:
async ngOnInit(): Promise<void> {
this.items = await this.itemService.getItems()
}
26) Replace item.service.ts
with the following:
import { Injectable } from '@angular/core'
import { knownFolders, path, getFileAccess } from '@nativescript/core'
import { getFile } from '@nativescript/core/http';
import { read, utils } from 'xlsx';
import { Item } from './item'
interface IPresident { Name: string; Index: number };
@Injectable({ providedIn: 'root' })
export class ItemService {
private items: Array<Item>;
async getItems(): Promise<Array<Item>> {
/* fetch https://sheetjs.com/pres.xlsx */
const temp: string = path.join(knownFolders.temp().path, "pres.xlsx");
const ab = await getFile("https://sheetjs.com/pres.xlsx", temp)
/* read the temporary file */
const wb = read(await getFileAccess().readBufferAsync(ab.path));
/* translate the first worksheet to the required Item type */
const data = utils.sheet_to_json<IPresident>(wb.Sheets[wb.SheetNames[0]]);
return this.items = data.map((pres, id) => ({id, name: pres.Name, role: ""+pres.Index} as Item));
}
getItem(id: number): Item {
return this.items.filter((item) => item.id === id)[0]
}
}
27) Relaunch the app in the Android simulator:
npx -p nativescript ns run android
The app should show Presidential data.
Android Device
28) Connect an Android device using a USB cable.
If the device asks to allow USB debugging, tap "Allow".
29) Close any Android / iOS emulators.
30) Build APK and run on device:
npx -p nativescript ns run android
If the Android emulators are closed and an Android device is connected, the last command will build an APK and install on the device.
iOS Device
31) Connect an iOS device using a USB cable
32) Close any Android / iOS emulators.
33) Enable developer code signing certificates9
34) Run on device:
npx -p nativescript ns run ios
- See
read
in "Reading Files"↩ - See "Workbook Object"↩
- See
sheet_to_json
in "Utilities"↩ - See
write
in "Writing Files"↩ - See
json_to_sheet
in "Utilities"↩ - See "Workbook Helpers" in "Utilities" for details on
book_new
andbook_append_sheet
.↩ - See
read
in "Reading Files"↩ - See "Local setup" in the NativeScript documentation. For Windows and Linux, follow the "Android" instructions. For macOS, follow both the iOS and Android instructions.↩
- The Flutter documentation covers the instructions in more detail. The correct workspace is
platforms/ios/SheetJSNS.xcodeproj/project.xcworkspace
↩