18 KiB
title | sidebar_label | pagination_prev | pagination_next | sidebar_position | sidebar_custom_props | ||
---|---|---|---|---|---|---|---|
Native Sheets in NativeScript | NativeScript | demos/static/index | demos/desktop/index | 2 |
|
import current from '/version.js'; import CodeBlock from '@theme/CodeBlock';
export const g = {style: {color:"green"}}; export const r = {style: {color:"red"}}; export const y = {style: {color:"yellow"}};
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 |
---|---|
:::info pass
The discussion covers the NativeScript + Angular integration. Familiarity with Angular and TypeScript is assumed.
:::
:::warning Telemetry
Before starting this demo, manually disable telemetry.
NativeScript 8.6.0 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
:::note
The project was last tested on 2023 October 12. NativeScript version
(as verified with npx -p nativescript ns --version
) was 8.6.0
.
The iOS demo was last tested on 2023-10-12 with @nativescript/ios
version 8.6.1
on an emulated iPhone 15 Pro Max + iOS 17.0
The Android demo was last tested on 2023-10-12 with @nativescript/android
version 8.6.2
on an emulated Pixel 3 + Android 13 ("Tiramisu") API 33.
:::
Platform Configuration
- Disable telemetry:
npx -p nativescript ns usage-reporting disable
npx -p nativescript ns error-reporting disable
- Follow the official Environment Setup instructions8.
:::caution pass
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
:::
- 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{'\n'} {'\n'} No issues were detected.{'\n'} ✔ Your ANDROID_HOME environment variable is set and points to correct directory.{'\n'} ✔ Your adb from the Android SDK is correctly installed.{'\n'} ✔ The Android SDK is installed.{'\n'} ✔ A compatible Android SDK for compilation is found.{'\n'} ✔ Javac is installed and is configured properly.{'\n'} ✔ The Java Development Kit (JDK) is installed and is configured properly.{'\n'} ✔ Getting NativeScript components versions information...{'\n'} ✔ Component nativescript has 8.6.0 version and is up to date.
- 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{'\n'} {'\n'} No issues were detected.{'\n'} ✔ Xcode is installed and is configured properly.{'\n'} ✔ xcodeproj is installed and is configured properly.{'\n'} ✔ CocoaPods are installed.{'\n'} ✔ CocoaPods update is not required.{'\n'} ✔ CocoaPods are configured properly.{'\n'} ✔ Your current CocoaPods version is newer than 1.0.0.{'\n'} ✔ Python installed and configured correctly.{'\n'} ✔ The Python 'six' package is found.{'\n'} ✔ Xcode version 15.0.0 satisfies minimum required version 10.{'\n'} ✔ Getting NativeScript components versions information...{'\n'} ✔ Component nativescript has 8.6.0 version and is up to date.
Base Project
- Create a skeleton NativeScript + Angular app:
npx -p nativescript ns create SheetJSNS --ng
- 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
- From the project folder, install the library:
{\ npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
}
Add SheetJS
:::note pass
The goal of this section is to display the SheetJS library version number.
:::
- Edit
src/app/item/items.component.ts
so that the component imports the SheetJS version string and adds it to aversion
variable in the component:
// highlight-next-line
import { version } from 'xlsx';
import { Component, OnInit } from '@angular/core'
// ...
export class ItemsComponent implements OnInit {
items: Array<Item>
// highlight-next-line
version = `SheetJS - ${version}`;
constructor(private itemService: ItemService) {}
// ...
- Edit the template
src/app/item/items.component.html
to referenceversion
in the title of the action bar:
<!-- highlight-next-line -->
<ActionBar [title]="version"></ActionBar>
<GridLayout>
<!-- ... -->
- Relaunch the app in the Android simulator:
npx -p nativescript ns run android
The title bar should show the version.
Local Files
- Add the Import and Export buttons to the template:
<ActionBar [title]="version"></ActionBar>
<!-- highlight-start -->
<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>
<!-- highlight-end -->
<ListView [items]="items">
<!-- ... -->
</ListView>
<!-- highlight-next-line -->
</StackLayout>
- Add the
import
andexport
methods in the component script:
// highlight-start
import { version, utils, read, write } from 'xlsx';
import { Dialogs, getFileAccess } from '@nativescript/core';
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
// highlight-end
import { Component, OnInit } from '@angular/core'
import { Item } from './item'
import { ItemService } from './item.service'
// highlight-start
function get_url_for_filename(filename: string): string {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
return path.normalize(target.path + "///" + filename);
}
// highlight-end
@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()
}
// highlight-start
/* Import button */
async import() {
}
/* Export button */
async export() {
}
// highlight-end
}
- Restart the app process. Two buttons should show up at the top:
- Implement import and export by adding the highlighted lines:
/* Import button */
async import() {
// highlight-start
/* 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); }
// highlight-end
}
/* Export button */
async export() {
// highlight-start
/* 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); }
// highlight-end
}
Android
- 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.
-
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
-
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
- 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
...
- 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"
- Tap "Import File". A dialog will print the path of the file that was read. The first item in the list will change.
iOS
- Launch the app in the iOS Simulator:
npx -p nativescript ns run ios
-
Tap "Export File". A dialog will print where the file was written.
-
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
...
-
Restart the app after saving the file.
-
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
- In
src/app/item/items.component.ts
, makengOnInit
asynchronous:
async ngOnInit(): Promise<void> {
this.items = await this.itemService.getItems()
}
- 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]
}
}
- Relaunch the app in the Android simulator:
npx -p nativescript ns run android
The app should show Presidential data.
-
See "Workbook Object" ↩︎
-
See "Workbook Helpers" in "Utilities" for details on
book_new
andbook_append_sheet
. ↩︎ -
See "Local setup" in the NativeScript documentation. For Windows and Linux, follow the "Android" instructions. For macOS, follow both the iOS and Android instructions. ↩︎