40 KiB
sidebar_position | title |
---|---|
19 | iOS and Android Apps |
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
Many mobile app frameworks mix JavaScript / CSS / HTML5 concepts with native extensions and libraries to create a hybrid development experience. Developers well-versed in web technologies can now build actual mobile applications that run on iOS and Android!
:::warning
The ecosystem has broken backwards-compatibility many times!
iOS and Android, as well as the underlying JavaScript frameworks, make breaking changes regularly. The demos were tested against emulators / real devices at some point in time. A framework or OS change can render the demos inoperable.
Each demo section will mention test dates and platform versions.
:::
The "JavaScript Engines" section includes samples for JavaScript engines used in the mobile app frameworks. SheetJS libraries have been tested in the relevant engines and should "just work" with some caveats.
:::caution readFile
and writeFile
XLSX.readFile
and XLSX.writeFile
do not work in mobile apps! The demos
include platform-specific details for fetching file data for XLSX.read
and
writing file data generated by XLSX.write
.
Some platforms provide this functionality as part of the standard library. Other platforms, including React Native, do not. When the platform does not provide, usually there are third-party modules to provide needed functionality.
:::
MacOS is required for the iOS demos. The Android demos were tested on MacOS.
React Native
:::note
This demo was tested on an Intel Mac on 2022 August 14 with RN 0.67.2
.
The iOS simulator runs iOS 15.5 on an iPhone 13.
The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5.
:::
:::warning
React Native does not provide a native file picker or a method for reading and writing data from documents on the devices. A third-party library must be used.
Since React Native internals change between releases, libraries may only work with specific versions of React Native. Project documentation should be consulted before picking a library.
:::
The following table lists tested file plugins. "OS" lists tested platforms ("A" for Android and "I" for iOS). "Copy" indicates whether an explicit copy is needed (file picker copies to cache directory and file plugin reads cache).
Filesystem Plugin | File Picker Plugin | OS | Copy |
---|---|---|---|
react-native-file-access |
react-native-document-picker |
AI |
|
react-native-blob-util |
react-native-document-picker |
AI |
YES |
rn-fetch-blob |
react-native-document-picker |
AI |
YES |
react-native-fs |
react-native-document-picker |
AI |
YES |
expo-file-system |
expo-document-picker |
I |
YES |
RN File Picker
The following libraries have been tested:
react-native-document-picker
Selecting a file (click to show)
When a copy is not needed:
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
const path = f.uri; // this path can be read by RN file plugins
When a copy is needed:
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
const path = f.fileCopyUri; // this path can be read by RN file plugins
expo-document-picker
Selecting a file (click to show)
When using DocumentPicker.getDocumentAsync
, enable copyToCacheDirectory
:
import * as DocumentPicker from 'expo-document-picker';
const result = await DocumentPicker.getDocumentAsync({
// highlight-next-line
copyToCacheDirectory: true,
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
});
const path = result.uri;
RN File Plugins
The following libraries have been tested:
react-native-blob-util
and rn-fetch-blob
:::note Historical Context
The react-native-fetch-blob
project was archived in 2019. At the time, there
were a number of project forks. The maintainers blessed the rn-fetch-blob
fork as the spiritual successor.
react-native-blob-util
is an active fork of rn-fetch-blob
On the day that this demo was tested (2022 August 14), both rn-fetch-blob
and
react-native-blob-util
worked with the tested iOS and Android SDK versions.
The APIs are identical for the purposes of working with files.
:::
The ascii
type returns an array of numbers corresponding to the raw bytes.
A Uint8Array
from the data is compatible with the buffer
type.
Reading and Writing snippets (click to show)
The snippets use rn-fetch-blob
. To use react-native-blob-util
, change the
import
statements to load the module.
Reading Data
import * as XLSX from "xlsx";
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { readFile } = RNFetchBlob.fs;
const res = await readFile(path, 'ascii');
const wb = XLSX.read(new Uint8Array(res), {type:'buffer'});
:::caution
On iOS, URIs from react-native-document-picker
must be massaged:
import { pickSingle } from 'react-native-document-picker';
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs;
const f = await pickSingle({
// highlight-start
// Instruct the document picker to copy file to Documents directory
copyTo: "documentDirectory",
// highlight-end
allowMultiSelection: false, mode: "open" });
// highlight-start
// `f.uri` is the original path and `f.fileCopyUri` is the path to the copy
let path = f.fileCopyUri;
// iOS workaround
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/");
// highlight-end
const res = await readFile(path, 'ascii');
:::
Writing Data
import * as XLSX from "xlsx";
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs;
const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"});
const file = DocumentDir + "/sheetjsw.xlsx";
const res = await writeFile(file, Array.from(wbout), 'ascii');
react-native-file-access
The base64
encoding returns strings compatible with the base64
type:
Reading and Writing snippets (click to show)
Reading Data
import * as XLSX from "xlsx";
import { FileSystem } from "react-native-file-access";
const b64 = await FileSystem.readFile(path, "base64");
/* b64 is a base64 string */
const workbook = XLSX.read(b64, {type: "base64"});
Writing Data
import * as XLSX from "xlsx";
import { Dirs, FileSystem } from "react-native-file-access";
const DDP = Dirs.DocumentDir + "/";
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a base64 string */
await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64");
react-native-fs
The ascii
encoding returns binary strings compatible with the binary
type:
Reading and Writing snippets (click to show)
Reading Data
import * as XLSX from "xlsx";
import { readFile } from "react-native-fs";
const bstr = await readFile(path, "ascii");
/* bstr is a binary string */
const workbook = XLSX.read(bstr, {type: "binary"});
Writing Data
import * as XLSX from "xlsx";
import { writeFile, DocumentDirectoryPath } from "react-native-fs";
const bstr = XLSX.write(workbook, {type:'binary', bookType:"xlsx"});
/* bstr is a binary string */
await writeFile(DocumentDirectoryPath + "/sheetjs.xlsx", bstr, "ascii");
expo-file-system
:::caution
Some Expo APIs return URIs that cannot be read with expo-file-system
. This
will manifest as an error:
Unsupported scheme for location '...'
The expo-document-picker
snippet makes a local copy.
:::
The EncodingType.Base64
encoding is compatible with base64
type.
Reading and Writing snippets (click to show)
Reading Data
Calling FileSystem.readAsStringAsync
with FileSystem.EncodingType.Base64
encoding returns a promise resolving to a string compatible with base64
type:
import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
const workbook = XLSX.read(b64, { type: "base64" });
Writing Data
The FileSystem.EncodingType.Base64
encoding accepts Base64 strings:
import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a base64 string */
await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 });
Demo
:::warning
There are many moving parts and pitfalls with React Native apps. It is strongly recommended to follow the official React Native tutorials for iOS and Android before approaching this demo. Details like creating an Android Virtual Device are not covered here.
:::
Complete Example (click to show)
This example tries to separate the library-specific functions.
- Follow the official React Native CLI Quickstart!
Quickstart URL: http://reactnative.dev/docs/environment-setup
Follow the instructions for iOS and for Android. They will cover installation and system configuration. By the end, you should be able to run the sample app in the Android and the iOS simulators.
- Create project:
npx react-native init SheetJSRN --version="0.67.2"
- Install shared dependencies:
cd SheetJSRN
curl -LO http://oss.sheetjs.com/assets/img/logo.png
npm i -S https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
npm i -S react-native-table-component react-native-document-picker
Refresh iOS project by running pod install
from the ios
subfolder:
cd ios
pod install
cd ..
- Download
index.js
and replace:
curl -LO https://docs.sheetjs.com/mobile/index.js
Start the iOS emulator:
npx react-native run-ios
You should see the skeleton app:
- Pick a filesystem library for integration:
Install react-native-blob-util
dependency:
npm i -S react-native-blob-util
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
async function pickAndParse() {
/* rn-fetch-blob / react-native-blob-util need a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await RNFetchBlob.fs.readFile(path, 'ascii');
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
return file;
}
// highlight-end
const make_width = ws => {
Install react-native-file-access
dependency:
npm i -S react-native-file-access
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Dirs, FileSystem } from 'react-native-file-access';
async function pickAndParse() {
/* react-native-file-access does not need a copy */
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
const res = await FileSystem.readFile(f.uri, "base64");
return read(res, {type: 'base64'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
const file = Dirs.DocumentDir + "/sheetjsw.xlsx";
await FileSystem.writeFile(file, wbout, "base64");
return file;
}
// highlight-end
const make_width = ws => {
Install rn-fetch-blob
dependency:
npm i -S rn-fetch-blob
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
async function pickAndParse() {
/* rn-fetch-blob / react-native-blob-util need a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await RNFetchBlob.fs.readFile(path, 'ascii');
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
return file;
}
// highlight-end
const make_width = ws => {
Install react-native-fs
dependency:
npm i -S react-native-fs
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs';
async function pickAndParse() {
/* react-native-fs needs a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
const bstr = await readFile(f.fileCopyUri, 'ascii');
return read(bstr, {type:'binary'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'binary', bookType:"xlsx"});
const file = DocumentDirectoryPath + "/sheetjsw.xlsx";
await writeFile(file, wbout, 'ascii');
return file;
}
// highlight-end
const make_width = ws => {
:::caution
At the time of testing, the npx install-expo-modules
step breaks the Android
project. The demo works as expected on iOS.
:::
Install expo-file-system
and expo-document-picker
dependencies:
npx install-expo-modules
npm i -S expo-file-system expo-document-picker
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
// highlight-start
import { read, write } from 'xlsx';
import { getDocumentAsync } from 'expo-document-picker';
import { documentDirectory, readAsStringAsync, writeAsStringAsync } from 'expo-file-system';
async function pickAndParse() {
const result = await getDocumentAsync({copyToCacheDirectory: true});
const path = result.uri;
const res = await readAsStringAsync(path, { encoding: "base64" });
return read(res, {type: 'base64'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
const file = documentDirectory + "sheetjsw.xlsx";
await writeAsStringAsync(file, wbout, { encoding: "base64" });
return file;
}
// highlight-end
const make_width = ws => {
- Refresh the app:
cd ios
pod install
cd ..
After doing this, the simulator must be stopped and the dev server must reload:
npx react-native run-ios
iOS Testing
The app can be tested with the following sequence in the simulator:
- Download https://sheetjs.com/pres.numbers
- In the simulator, click the Home icon to return to the home screen
- Click on the "Files" icon
- Click and drag
pres.numbers
from a Finder window into the simulator.
- Make sure "On My iPhone" is highlighted and select "Save"
- Click the Home icon again then select the
SheetJSRN
app - Click "Import data" and select
pres
:
Once selected, the screen should refresh with new contents:
- Click "Export data". You will see a popup with a location:
- Find the file and verify the contents are correct:
find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
Once testing is complete, stop the simulator and the dev process.
Android Testing
There are no Android-specific steps. Emulator can be started with:
npx react-native run-android
The app can be tested with the following sequence in the simulator:
- Download https://sheetjs.com/pres.numbers
- Click and drag
pres.numbers
from a Finder window into the simulator. - Click "Import data" and select
pres.numbers
:
Once selected, the screen should refresh with new contents:
- Click "Export data". You will see a popup with a location:
- Pull the file from the simulator and verify the contents:
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
npx xlsx-cli /tmp/sheetjsw.xlsx
NativeScript
:::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.
:::
:::warning Binary Data issues
NativeScript will not safely transmit binary or UTF8 strings. XLSB, NUMBERS, XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled.
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.
:::
The @nativescript/core/file-system
package provides classes for file access.
Integration Details
Reading and writing data require a file handle. The following snippet searches typical document folders for a specified filename:
import { File, Folder, knownFolders, path } from '@nativescript/core/file-system';
function get_handle_for_filename(filename: string): File {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
const url: string = path.normalize(target.path + "///" + filename);
return File.fromPath(url);
}
The encoding ISO_8859_1
spiritually resembles the "binary"
SheetJS type
Reading data
File#readText(encoding.ISO_8859_1)
returns strings compatible with "binary"
/* get binary string */
const bstr: string = await file.readText(encoding.ISO_8859_1);
/* read workbook */
const wb = read(bstr, { type: "binary" });
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:
/* generate binary string */
const bstr: string = write(wb, { bookType: 'csv', type: 'binary' });
/* attempt to save binary string to file */
await file.writeText(bstr, encoding.ISO_8859_1);
Demo
The demo builds off of the NativeScript + Angular example. Familiarity with with Angular and TypeScript is assumed.
Complete Example (click to show)
-
Follow the official Environment Setup instructions (tested with "MacOS + iOS")
-
Create a skeleton NativeScript + Angular app:
ns create SheetJSNS --ng
- Launch the app in the iOS simulator to verify that the demo built properly:
cd SheetJSNS
ns run ios
(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-latest/xlsx-latest.tgz
- 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:
// 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:
<!-- 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.
- 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">
<ng-template let-item="item">
<StackLayout [nsRouterLink]="['/item', item.id]">
<Label [text]="item.name"></Label>
</StackLayout>
</ng-template>
</ListView>
<!-- highlight-next-line -->
</StackLayout>
// 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';
// highlight-end
import { Component, OnInit } from '@angular/core'
import { Item } from './item'
import { ItemService } from './item.service'
// highlight-start
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];
}
// 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 and two buttons should show up at the top:
- Implement import and export:
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");
try {
/* get binary string */
const bstr: string = await file.readText(encoding.ISO_8859_1);
/* read workbook */
const wb = read(bstr, { type: "binary" });
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* 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); }
// highlight-end
}
/* Export button */
async export() {
// highlight-start
/* find appropriate path */
const [file, url] = get_handle_for_filename("SheetJSNS.csv");
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 binary string */
const wbout: string = write(wb, { bookType: 'csv', type: 'binary' });
/* 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); }
// highlight-end
}
}
Restart the app process.
Testing
The app can be tested with the following sequence in the simulator:
-
Hit "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:
id,name,role
1,Ter Stegen,Goalkeeper
3,Piqué,Defender
4,I. Rakitic,Midfielder
...
After the header row, add the line 0,SheetJS,Library
:
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. The first item in the list will change:
Quasar
:::note
This demo was tested on an Intel Mac on 2022 August 14. Quasar version 2.7.7
.
The iOS simulator runs iOS 15.5 on an iPhone SE 3rd generation.
:::
This demo will use the Quasar ViteJS starter project with VueJS and Cordova.
Integration Details
The complete solution uses cordova-plugin-file
for file operations. It can
be installed like any other Cordova plugin:
cd src-cordova
cordova plugin add cordova-plugin-file
cd ..
Reading data
The q-file
component presents an API reminiscent of File Input elements:
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
When binding to the input
element, the callback receives an Event
object:
import { read } from 'xlsx';
// assuming `todos` is a standard VueJS `ref`
async function updateFile(v) { try {
// `v.target.files[0]` is the desired file object
const files = (v.target as HTMLInputElement).files;
if(!files || files.length == 0) return;
// read first file
const wb = read(await files[0].arrayBuffer());
// get data of first worksheet as an array of objects
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
// update state
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
} catch(e) { console.log(e); } }
Writing data
The API is shaped like the File and Directory Entries API. For clarity, since the code is a "pyramid of doom", the error handlers are omitted:
import { write } from 'xlsx';
// on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array`
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
// Request filesystem access for persistent storage
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
// Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
// Request a FileWriter for writing data
entry.createWriter(writer => {
// The FileWriter API needs an actual Blob
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
// This callback is called if the write is successful
writer.onwriteend = () => {
// TODO: show a dialog
};
// writer.onerror will be invoked if there is an error in writing
// write the data
writer.write(data);
});
});
});
Demo
The demo builds off of the Vite example. Familiarity with VueJS and TypeScript is assumed.
Complete Example (click to show)
- Ensure all of the dependencies are installed. Install the CLI globally:
npm i -g @quasar/cli cordova
(you may need to run sudo npm i -g
if there are permission issues)
- Create a new app:
npm init quasar
When prompted:
- "What would you like to build?":
App with Quasar CLI
- "Project folder":
SheetJSQuasar
- "Pick Quasar version":
Quasar v2 (Vue 3 | latest and greatest)
- "Pick script type":
Typescript
- "Pick Quasar App CLI variant":
Quasar App CLI with Vite
- "Package name": (just press enter, it will use the default
sheetjsquasar
- "Project product name":
SheetJSQuasar
- "Project description":
SheetJS + Quasar
- "Author": (just press enter, it will use your git config settings)
- "Pick a Vue component style":
Composition API
- "Pick your CSS preprocessor":
None
- "Check the features needed for your project": Deselect everything
- "Install project dependencies":
No
- Install dependencies:
cd SheetJSQuasar
npm i
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
- Set up Cordova:
quasar mode add cordova
When prompted, enter the app id org.sheetjs.quasar
.
It will create a new src-cordova
folder. Continue in that folder:
cd src-cordova
cordova platform add ios
cordova plugin add cordova-plugin-wkwebview-engine
cordova plugin add cordova-plugin-file
:::note
If there is an error Could not load API for iOS project
, it needs to be reset:
cordova platform rm ios
cordova platform add ios
cordova plugin add cordova-plugin-file
:::
Return to the project directory:
cd ..
- Start the dev server:
quasar dev -m ios
:::caution
If the app is blank, delete the app and close the simulator, then restart dev
:::
- Add the Dialog plugin to
quasar.config.js
:
// Quasar plugins
// highlight-next-line
plugins: ['Dialog']
- In the template section of
src/pages/IndexPage.vue
, add a Save button and a Load file picker component at the bottom of the page:
<!-- highlight-start -->
<q-btn-group>
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
<q-btn label="Save File" @click="saveFile" />
</q-btn-group>
<!-- highlight-end -->
</q-page>
</template>
This uses two functions that should be added to the component script:
const meta = ref<Meta>({
totalCount: 1200
});
// highlight-start
function saveFile() {
}
async function updateFile(v) {
}
return { todos, meta, saveFile, updateFile };
// highlight-end
}
});
The app should now show two buttons at the bottom:
:::caution
If the app is blank or not refreshing, delete the app and close the simulator, then restart the dev process.
:::
- Wire up the
updateFile
function:
import { defineComponent, ref } from 'vue';
// highlight-start
import { read, write, utils } from 'xlsx';
import { useQuasar } from 'quasar';
// highlight-end
export default defineComponent({
// ...
// highlight-start
const $q = useQuasar();
function dialogerr(e) { $q.dialog({title: "Error!", message: e.message || String(e)}); }
// highlight-end
function saveFile() {
}
async function updateFile(v) {
// highlight-start
try {
const files = (v.target as HTMLInputElement).files;
if(!files || files.length == 0) return;
const wb = read(await files[0].arrayBuffer());
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
} catch(e) { dialogerr(e); }
// highlight-end
}
To test that reading works:
- Download https://sheetjs.com/pres.numbers
- In the simulator, click the Home icon to return to the home screen
- Click on the "Files" icon
- Click and drag
pres.numbers
from a Finder window into the simulator.
- Make sure "On My iPhone" is highlighted and select "Save"
- Click the Home icon again then select the
SheetJSQuasar
app - Click the "Load" button, then select "Choose File" and select
pres
:
Once selected, the screen should refresh with new contents:
- Wire up the
saveFile
function:
function saveFile() {
// highlight-start
/* generate workbook from state */
const ws = utils.json_to_sheet(todos.value);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "SheetJSQuasar");
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
/* save to file */
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
try {
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
const msg = `File stored at ${$q.cordova.file.documentsDirectory} ${entry.fullPath}`;
entry.createWriter(writer => {
try {
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
writer.onwriteend = () => {
try {
$q.dialog({title: "Success!", message: msg});
} catch(e) { dialogerr(e); }
};
writer.onerror = dialogerr;
writer.write(data);
} catch(e) { dialogerr(e); }
}, dialogerr);
}, dialogerr);
} catch(e) { dialogerr(e) }
}, dialogerr);
// highlight-end
}
The page should revert to the old contents.
To test that writing works:
- Click "Save File". You will see a popup with a location:
- Find the file and verify the contents are correct. Run in a new terminal:
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
Since the contents reverted, you should see
SheetJSQuasar
id,content
1,ct1
2,ct2
3,ct3
4,ct4
5,ct5
-
Use "Load File" to select
pres.numbers
again. Wait for the app to refresh. -
Click "Save File", then re-run the command:
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
The contents from pres.numbers
should show up now, with a new header row:
SheetJSQuasar
id,content
42,Bill Clinton
43,GeorgeW Bush
44,Barack Obama
45,Donald Trump
46,Joseph Biden
Ionic
:::note
This demo was tested on an Intel Mac on 2022 August 18 with Cordova backend.
The file integration uses @ionic-native/file
version 5.36.0
.
The iOS simulator runs iOS 15.5 on an iPod Touch 7th Gen.
:::
:::warning Telemetry
Before starting this demo, manually disable telemetry. On Linux and MacOS:
rm -rf ~/.ionic/
mkdir ~/.ionic
cat <<EOF > ~/.ionic/config.json
{
"version": "6.20.1",
"telemetry": false,
"npmClient": "npm"
}
EOF
npx @capacitor/cli telemetry off
To verify telemetry was disabled:
npx @ionic/cli config get -g telemetry
npx @capacitor/cli telemetry
:::
Cordova
:::caution
The latest version of Ionic uses CapacitorJS. These notes are for Cordova apps.
:::
Array<Array<any>>
neatly maps to a table with ngFor
:
<ion-grid>
<ion-row *ngFor="let row of data">
<ion-col *ngFor="let val of row">
{{val}}
</ion-col>
</ion-row>
</ion-grid>
@ionic-native/file
reads and writes files on devices. readAsArrayBuffer
returns ArrayBuffer
objects suitable for array
type, and array
type can
be converted to blobs that can be exported with writeFile
:
/* read a workbook */
const ab: ArrayBuffer = await this.file.readAsArrayBuffer(url, filename);
const wb: XLSX.WorkBook = XLSX.read(bstr, {type: 'array'});
/* write a workbook */
const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
let blob = new Blob([wbout], {type: 'application/octet-stream'});
this.file.writeFile(url, filename, blob, {replace: true});
Demo
The demo uses Cordova.
Complete Example (click to show)
- Disable telemetry as noted in the warning.
Install required global dependencies:
npm i -g cordova-res @angular/cli native-run
Follow the React Native demo to ensure iOS and Android sims are ready.
- Create a new project:
npx @ionic/cli start SheetJSIonic blank --type angular --cordova --quiet --no-git --no-link --confirm
If a prompt discusses Cordova and Capacitor, enter Yes
to continue.
If a prompt asks about creating an Ionic account, enter N
to opt out.
- Set up Cordova:
npx @ionic/cli cordova platform add ios --confirm
npx @ionic/cli cordova plugin add cordova-plugin-file
npm install --save @ionic-native/core @ionic-native/file @ionic/cordova-builders
- Install dependencies:
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
- Add
@ionic-native/file
to the module. Differences highlighted below:
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
// highlight-next-line
import { File } from '@ionic-native/file/ngx';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
// highlight-next-line
providers: [File, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
- Download
home.page.ts
and replace:
curl -o src/app/home/home.page.ts -L https://docs.sheetjs.com/ionic/home.page.ts
- Test the app:
npx @ionic/cli cordova emulate ios