ns-binary
| @ -10,78 +10,83 @@ sidebar_custom_props: | ||||
| The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported | ||||
| from the main entrypoint or any script in the project. | ||||
| 
 | ||||
| :::warning Binary Data issues | ||||
| The "Complete Example" creates an app that looks like the screenshots below: | ||||
| 
 | ||||
| NativeScript will not safely transmit binary or UTF-8 strings. XLSB, NUMBERS, | ||||
| XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled. | ||||
| <table><thead><tr> | ||||
|   <th><a href="#demo">iOS</a></th> | ||||
| </tr></thead><tbody><tr><td> | ||||
| 
 | ||||
| This is a known NativeScript bug. | ||||
|  | ||||
| 
 | ||||
| This demo will focus on ASCII CSV files.  Once the bug is resolved, XLSX and | ||||
| other formats will be supported. | ||||
| 
 | ||||
| ::: | ||||
| </td></tr></tbody></table> | ||||
| 
 | ||||
| ## Integration Details | ||||
| 
 | ||||
| The `@nativescript/core/file-system` package provides classes for file access. | ||||
| The discussion covers the NativeScript + Angular integration.  Familiarity with | ||||
| Angular and TypeScript is assumed. | ||||
| 
 | ||||
| Reading and writing data require a file handle.  The following snippet searches | ||||
| typical document folders for a specified filename: | ||||
| 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: | ||||
| 
 | ||||
| ```ts | ||||
| import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| import { Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| 
 | ||||
| function get_handle_for_filename(filename: string): File { | ||||
| function get_url_for_filename(filename: string): string { | ||||
|   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | ||||
|   const url: string = path.normalize(target.path + "///" + filename); | ||||
|   return File.fromPath(url); | ||||
|   return path.normalize(target.path + "///" + filename); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| The encoding `ISO_8859_1` spiritually resembles the `"binary"` SheetJS type | ||||
| #### Reading data | ||||
| 
 | ||||
| **Reading data** | ||||
| 
 | ||||
| `File#readText(encoding.ISO_8859_1)` returns strings compatible with `"binary"` | ||||
| `getFileAccess().readBufferAsync` can read data: | ||||
| 
 | ||||
| ```ts | ||||
| /* get binary string */ | ||||
| const bstr: string = await file.readText(encoding.ISO_8859_1); | ||||
| import { getFileAccess } from '@nativescript/core'; | ||||
| 
 | ||||
| /* 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(bstr, { type: "binary" }); | ||||
| const wb = read(ab); | ||||
| ``` | ||||
| 
 | ||||
| **Writing data** | ||||
| #### Writing data | ||||
| 
 | ||||
| `File#writeText` with the `ISO_8859_1` encoding accepts `"binary"` strings with | ||||
| the caveat listed in the warning at the top of this section: | ||||
| `getFileAccess().writeBufferAsync` can write data: | ||||
| 
 | ||||
| ```ts | ||||
| /* generate binary string */ | ||||
| const bstr: string = write(wb, { bookType: 'csv', type: 'binary' }); | ||||
| import { getFileAccess } from '@nativescript/core'; | ||||
| 
 | ||||
| /* attempt to save binary string to file */ | ||||
| await file.writeText(bstr, encoding.ISO_8859_1); | ||||
| /* 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, u8); | ||||
| ``` | ||||
| 
 | ||||
| ## Demo | ||||
| 
 | ||||
| The demo builds off of the NativeScript + Angular example.  Familiarity with | ||||
| Angular and TypeScript is assumed. | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| This demo was tested on an Intel Mac on 2022 August 10.  NativeScript version | ||||
| (as verified with `ns --version`) is `8.3.2`.  The iOS simulator runs iOS 15.5 | ||||
| on an iPhone SE 3rd generation. | ||||
| This demo was tested on an Intel Mac on 2023 April 03.  NativeScript version | ||||
| (as verified with `ns --version`) is `8.5.1`. | ||||
| 
 | ||||
| The iOS simulator runs iOS 16.2 on an iPhone 14 Pro Max. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| <details><summary><b>Complete Example</b> (click to show)</summary> | ||||
| 
 | ||||
| 0) Follow the official Environment Setup instructions (tested with "MacOS + iOS") | ||||
| 0) Follow the official Environment Setup instructions | ||||
| 
 | ||||
| 1) Create a skeleton NativeScript + Angular app: | ||||
| 
 | ||||
| @ -110,48 +115,32 @@ npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz | ||||
| 4) To confirm the library was loaded, change the title to show the version.  The | ||||
| differences are highlighted. | ||||
| 
 | ||||
| `src/app/item/items.component.ts` imports the version string to the component: | ||||
| `src/app/item/items.component.ts` should import the version string: | ||||
| 
 | ||||
| ```ts title="src/app/item/items.component.ts" | ||||
| // highlight-next-line | ||||
| import { version } from 'xlsx'; | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| 
 | ||||
| import { Item } from './item' | ||||
| import { ItemService } from './item.service' | ||||
| // ... | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'ns-items', | ||||
|   templateUrl: './items.component.html', | ||||
| }) | ||||
| export class ItemsComponent implements OnInit { | ||||
|   items: Array<Item> | ||||
|   // highlight-next-line | ||||
|   version = `SheetJS - ${version}`; | ||||
| 
 | ||||
|   constructor(private itemService: ItemService) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.items = this.itemService.getItems() | ||||
|   } | ||||
| } | ||||
| // ... | ||||
| ``` | ||||
| 
 | ||||
| `src/app/item/items.component.html` references the version in the title: | ||||
| `src/app/item/items.component.html` should use the version in the title: | ||||
| 
 | ||||
| ```xml title="src/app/item/items.component.html" | ||||
| <!-- highlight-next-line --> | ||||
| <ActionBar [title]="version"></ActionBar> | ||||
| 
 | ||||
| <GridLayout> | ||||
|   <ListView [items]="items"> | ||||
|     <ng-template let-item="item"> | ||||
|       <StackLayout [nsRouterLink]="['/item', item.id]"> | ||||
|         <Label [text]="item.name"></Label> | ||||
|       </StackLayout> | ||||
|     </ng-template> | ||||
|   </ListView> | ||||
| </GridLayout> | ||||
| <!-- ... --> | ||||
| ``` | ||||
| 
 | ||||
| Relaunch the app with `ns run ios` and the title bar should show the version. | ||||
| @ -171,11 +160,7 @@ Relaunch the app with `ns run ios` and the title bar should show the version. | ||||
|   </StackLayout> | ||||
| <!-- highlight-end --> | ||||
|   <ListView [items]="items"> | ||||
|     <ng-template let-item="item"> | ||||
|       <StackLayout [nsRouterLink]="['/item', item.id]"> | ||||
|         <Label [text]="item.name"></Label> | ||||
|       </StackLayout> | ||||
|     </ng-template> | ||||
|     <!-- ... --> | ||||
|   </ListView> | ||||
| <!-- highlight-next-line --> | ||||
| </StackLayout> | ||||
| @ -184,9 +169,8 @@ Relaunch the app with `ns run ios` and the title bar should show the version. | ||||
| ```ts title="src/app/item/items.component.ts" | ||||
| // highlight-start | ||||
| import { version, utils, read, write } from 'xlsx'; | ||||
| import { Dialogs } from '@nativescript/core'; | ||||
| import { encoding } from '@nativescript/core/text'; | ||||
| import { File, Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| import { Dialogs, getFileAccess } from '@nativescript/core'; | ||||
| import { Folder, knownFolders, path } from '@nativescript/core/file-system'; | ||||
| // highlight-end | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| 
 | ||||
| @ -194,10 +178,9 @@ import { Item } from './item' | ||||
| import { ItemService } from './item.service' | ||||
| 
 | ||||
| // highlight-start | ||||
| function get_handle_for_filename(filename: string): [File, string] { | ||||
| function get_url_for_filename(filename: string): string { | ||||
|   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | ||||
|   const url: string = path.normalize(target.path + "///" + filename); | ||||
|   return [File.fromPath(url), url]; | ||||
|   return path.normalize(target.path + "///" + filename); | ||||
| } | ||||
| // highlight-end | ||||
| 
 | ||||
| @ -231,50 +214,22 @@ Restart the app process and two buttons should show up at the top: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 6) Implement import and export: | ||||
| 6) Implement import and export by adding the highlighted lines: | ||||
| 
 | ||||
| ```ts title="src/app/item/items.component.ts" | ||||
| import { version, utils, read, write } from 'xlsx'; | ||||
| import { Dialogs } from '@nativescript/core'; | ||||
| import { encoding } from '@nativescript/core/text'; | ||||
| import { File, 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_handle_for_filename(filename: string): [File, string] { | ||||
|   const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic(); | ||||
|   const url: string = path.normalize(target.path + "///" + filename); | ||||
|   return [File.fromPath(url), url]; | ||||
| } | ||||
| 
 | ||||
| @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() { | ||||
|     // highlight-start | ||||
|     /* find appropriate path */ | ||||
|     const [file, url] = get_handle_for_filename("SheetJSNS.csv"); | ||||
|     const url = get_url_for_filename("SheetJSNS.xls"); | ||||
| 
 | ||||
|     try { | ||||
|       /* get binary string */ | ||||
|       const bstr: string = await file.readText(encoding.ISO_8859_1); | ||||
|       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(bstr, { type: "binary" }); | ||||
|       const wb = read(ab); | ||||
| 
 | ||||
|       /* grab first sheet */ | ||||
|       const wsname: string = wb.SheetNames[0]; | ||||
| @ -282,8 +237,7 @@ export class ItemsComponent implements OnInit { | ||||
| 
 | ||||
|       /* update table */ | ||||
|       this.items = utils.sheet_to_json<Item>(ws); | ||||
|       Dialogs.alert(`Attempting to read to ${filename} in ${url}`); | ||||
|     } catch(e) { Dialogs.alert(e.message); } | ||||
|     } catch(e) { await Dialogs.alert(e.message); } | ||||
|     // highlight-end | ||||
|   } | ||||
| 
 | ||||
| @ -291,7 +245,7 @@ export class ItemsComponent implements OnInit { | ||||
|   async export() { | ||||
|     // highlight-start | ||||
|     /* find appropriate path */ | ||||
|     const [file, url] = get_handle_for_filename("SheetJSNS.csv"); | ||||
|     const url = get_url_for_filename("SheetJSNS.xls"); | ||||
| 
 | ||||
|     try { | ||||
|       /* create worksheet from data */ | ||||
| @ -301,50 +255,41 @@ export class ItemsComponent implements OnInit { | ||||
|       const wb = utils.book_new(); | ||||
|       utils.book_append_sheet(wb, ws, "Sheet1"); | ||||
| 
 | ||||
|       /* generate binary string */ | ||||
|       const wbout: string = write(wb, { bookType: 'csv', type: 'binary' }); | ||||
|       /* generate Uint8Array */ | ||||
|       const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'buffer' }); | ||||
| 
 | ||||
|       /* attempt to save binary string to file */ | ||||
|       await file.writeText(wbout, encoding.ISO_8859_1); | ||||
|       Dialogs.alert(`Wrote to ${filename} in ${url}`); | ||||
|     } catch(e) { Dialogs.alert(e.message); } | ||||
|       /* attempt to save Uint8Array to file */ | ||||
|       await getFileAccess().writeBufferAsync(url, u8); | ||||
|       await Dialogs.alert(`Wrote to SheetJSNS.xls at ${url}`); | ||||
|     } catch(e) { await Dialogs.alert(e.message); } | ||||
|     // highlight-end | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Restart the app process. | ||||
| ### iOS | ||||
| 
 | ||||
| **Testing** | ||||
| Relaunch the app with `ns run ios` | ||||
| 
 | ||||
| The app can be tested with the following sequence in the simulator: | ||||
| 
 | ||||
| - Hit "Export File".  A dialog will print where the file was written | ||||
| - Tap "Export File".  A dialog will print where the file was written | ||||
| 
 | ||||
| - Open that file with a text editor.  It will be a 3-column CSV: | ||||
| - Open the file with a spreadsheet editor. | ||||
| 
 | ||||
| ```csv | ||||
| id,name,role | ||||
| 1,Ter Stegen,Goalkeeper | ||||
| 3,Piqué,Defender | ||||
| 4,I. Rakitic,Midfielder | ||||
| 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 | ||||
| ... | ||||
| ``` | ||||
| 
 | ||||
| After the header row, add the line `0,SheetJS,Library`: | ||||
| Restart the app after saving the file. | ||||
| 
 | ||||
| ```csv | ||||
| id,name,role | ||||
| 0,SheetJS,Library | ||||
| 1,Ter Stegen,Goalkeeper | ||||
| 3,Piqué,Defender | ||||
| ... | ||||
| ``` | ||||
| 
 | ||||
| - Hit "Import File".  A dialog will print the path of the file that was read. | ||||
| - Tap "Import File".  A dialog will print the path of the file that was read. | ||||
|   The first item in the list will change: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
|  | ||||
| @ -56,7 +56,4 @@ should support and investigate community modules.  If there are popular modules | ||||
| for features that must be included, or for teams that are comfortable with | ||||
| native app development, React Native is the obvious choice. | ||||
| 
 | ||||
| NativeScript is not recommended at this time.  There are known bugs related to | ||||
| binary data processing. The demo only supports plaintext file formats like CSV. | ||||
| 
 | ||||
| ::: | ||||
|  | ||||
| @ -521,7 +521,7 @@ sequenceDiagram | ||||
|     deactivate Page | ||||
|   end | ||||
|   Worker->>User: finish download | ||||
|   Worker->>Page: send competion message | ||||
|   Worker->>Page: send completion message | ||||
|   deactivate Worker | ||||
|   activate Page | ||||
|   Page->>User: download complete | ||||
|  | ||||
| Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 25 KiB | 
| Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 42 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docz/static/mobile/nsios.png
									
									
									
									
									
										Normal file
									
								
							
							
								
									
								
								
								
								
								
									
									
								
							
						
						| After Width: | Height: | Size: 74 KiB |