docs.sheetjs.com/docz/docs/03-demos/05-mobile/02-nativescript.md
2023-10-13 06:04:30 -04:00

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
summary
JS + Native Elements

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

iOS screenshot

Android screenshot

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

  1. Disable telemetry:
npx -p nativescript ns usage-reporting disable
npx -p nativescript ns error-reporting disable
  1. 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 Level 33
  • Android SDK Build-Tools Version 33.0.2

:::

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

  1. Create a skeleton NativeScript + Angular app:
npx -p nativescript ns create SheetJSNS --ng
  1. 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

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

:::

  1. 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:
// 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) {}
// ...
  1. Edit the template src/app/item/items.component.html to reference version in the title of the action bar:
<!-- highlight-next-line -->
<ActionBar [title]="version"></ActionBar>

<GridLayout>
<!-- ... -->
  1. Relaunch the app in the Android simulator:
npx -p nativescript ns run android

The title bar should show the version.

NativeScript Step 4

Local Files

  1. 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>
  1. Add the import and export 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
}
  1. Restart the app process. Two buttons should show up at the top:

NativeScript Step 5

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

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

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

  2. 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
  1. 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
...
  1. 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"
  1. Tap "Import File". A dialog will print the path of the file that was read. The first item in the list will change.

iOS

  1. Launch the app in the iOS Simulator:
npx -p nativescript ns run ios
  1. Tap "Export File". A dialog will print where the file was written.

  2. 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
...
  1. Restart the app after saving the file.

  2. Tap "Import File". A dialog will print the path of the file that was read. The first item in the list will change:

NativeScript Step 7

Fetching Files

  1. In src/app/item/items.component.ts, make ngOnInit asynchronous:
  async ngOnInit(): Promise<void> {
    this.items = await this.itemService.getItems()
  }
  1. 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]
  }
}
  1. Relaunch the app in the Android simulator:
npx -p nativescript ns run android

The app should show Presidential data.


  1. See read in "Reading Files" ↩︎

  2. See "Workbook Object" ↩︎

  3. See sheet_to_json in "Utilities" ↩︎

  4. See write in "Writing Files" ↩︎

  5. See json_to_sheet in "Utilities" ↩︎

  6. See "Workbook Helpers" in "Utilities" for details on book_new and book_append_sheet. ↩︎

  7. See read in "Reading Files" ↩︎

  8. See "Local setup" in the NativeScript documentation. For Windows and Linux, follow the "Android" instructions. For macOS, follow both the iOS and Android instructions. ↩︎