docs.sheetjs.com/docz/docs/03-demos/16-desktop.md
2022-08-25 04:22:28 -04:00

14 KiB

sidebar_position title
16 Desktop Applications

Web technologies like JavaScript and HTML have been adapted to the traditional app space. Typically these frameworks bundle a JavaScript engine as well as a windowing framework. SheetJS is compatible with many app frameworks.

NW.js

The Standalone scripts can be referenced in a SCRIPT tag from the entry point HTML page.

This demo was tested against NW.js 0.66.0.

Complete Example (click to show)
  1. Create a package.json file that specifies the entry point:
{
  "name": "sheetjs-nwjs",
  "author": "sheetjs",
  "version": "0.0.0",
  "main": "index.html",
  "dependencies": {
    "nw": "~0.66.0",
    "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz"
  }
}
  1. Download index.html into the same folder.

:::caution

Right-click the link and select "Save Link As...". Left-clicking the link will try to load the page in your browser. The goal is to save the file contents.

:::

  1. Run npm install to install dependencies

  2. To verify the app works, run in the test environment:

npx nw .

The app will show and you should be able to verify reading and writing by using the file input element to select a spreadsheet and clicking the export button.

  1. To build a standalone app, run the builder:
npx -p nw-builder nwbuild --mode=build .

This will generate the standalone app in the build\sheetjs-nwjs\ folder.

Reading data

The standard HTML5 FileReader techniques from the browser apply to NW.js!

NW.js handles the OS minutiae for dragging files into app windows. The drag and drop snippet apply to DIV elements on the page.

Similarly, file input elements automatically map to standard Web APIs.

For example, assuming a file input element on the page:

<input type="file" name="xlfile" id="xlf" />

The event handler would process the event as if it were a web event:

async function handleFile(e) {
  const file = e.target.files[0];
  const data = await file.arrayBuffer();
  /* data is an ArrayBuffer */
  const workbook = XLSX.read(data);

  /* DO SOMETHING WITH workbook HERE */
}
document.getElementById("xlf").addEventListener("change", handleFile, false);

Writing data

File input elements with the attribute nwsaveas show UI for saving a file. The standard trick is to generate a hidden file input DOM element and "click" it. Since NW.js does not present a writeFileSync in the fs package, a manual step is required:

/* pre-build the hidden nwsaveas input element */
var input = document.createElement('input');
input.style.display = 'none';
input.setAttribute('nwsaveas', 'SheetJSNWDemo.xlsx');
input.setAttribute('type', 'file');
document.body.appendChild(input);

/* show a message if the save is canceled */
input.addEventListener('cancel',function(){ alert("Save was canceled!"); });

/* write to a file on the 'change' event */
input.addEventListener('change',function(e){
  /* the `value` is the path that the program will write */
  var filename = this.value;

  /* use XLSX.write with type "buffer" to generate a buffer" */
  /* highlight-next-line */
  var wbout = XLSX.write(workbook, {type:'buffer', bookType:"xlsx"});
  /* highlight-next-line */
  fs.writeFile(filename, wbout, function(err) {
    if(!err) return alert("Saved to " + filename);
    alert("Error: " + (err.message || err));
  });
});

input.click();

Electron

The NodeJS Module can be imported from the main or the renderer thread.

Electron presents a fs module. The require('xlsx') call loads the CommonJS module, so XLSX.readFile and XLSX.writeFile work in the renderer thread.

This demo was tested against Electron 19.0.5 on an Intel Mac (darwin-x64).

Complete Example (click to show)

This demo includes a drag-and-drop box as well as a file input box, mirroring the SheetJS Data Preview Live Demo

The core data in this demo is an editable HTML table. The readers build up the table using sheet_to_html (with editable:true option) and the writers scrape the table using table_to_book.

The demo project is wired for electron-forge to build the standalone binary.

  1. Download the demo files:

:::caution

Right-click each link and select "Save Link As...". Left-clicking a link will try to load the page in your browser. The goal is to save the file contents.

:::

  1. Run npm install to install dependencies.

  2. To verify the app works, run in the test environment:

npx -y electron .

The app will show and you should be able to verify reading and writing by using the relevant buttons to open files and clicking the export button.

  1. To build a standalone app, run the builder:
npm run make

This will generate the standalone app in the out\sheetjs-electron-... folder. For a recent Intel Mac, the path will be out/sheetjs-electron-darwin-x64/

Writing Files

XLSX.writeFile writes workbooks to the file system. showSaveDialog shows a Save As dialog and returns the selected file name:

/* from the renderer thread */
const electron = require('@electron/remote');

/* this function will show the save dialog and try to write the workbook */
async function exportFile(workbook) {
  /* show Save As dialog */
  const result = await electron.dialog.showSaveDialog({
    title: 'Save file as',
    filters: [{
      name: "Spreadsheets",
      extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
    }]
  });
  /* write file */
  // highlight-next-line
  XLSX.writeFile(workbook, result.filePath);
}

:::note

In older versions of Electron, showSaveDialog returned the path directly:

var dialog = require('electron').remote.dialog;

function exportFile(workbook) {
  var result = dialog.showSaveDialog();
  XLSX.writeFile(workbook, result);
}

:::

Reading Files

Electron offers 3 different ways to read files, two of which use Web APIs.

File Input Element

File input elements automatically map to standard Web APIs.

For example, assuming a file input element on the page:

<input type="file" name="xlfile" id="xlf" />

The event handler would process the event as if it were a web event:

async function handleFile(e) {
  const file = e.target.files[0];
  const data = await file.arrayBuffer();
  /* data is an ArrayBuffer */
  const workbook = XLSX.read(data);

  /* DO SOMETHING WITH workbook HERE */
}
document.getElementById("xlf").addEventListener("change", handleFile, false);

Drag and Drop

The drag and drop snippet applies to DIV elements on the page.

For example, assuming a DIV on the page:

<div id="drop">Drop a spreadsheet file here to see sheet data</div>

The event handler would process the event as if it were a web event:

async function handleDrop(e) {
  e.stopPropagation();
  e.preventDefault();

  const file = e.dataTransfer.files[0];
  const data = await file.arrayBuffer();
  /* data is an ArrayBuffer */
  const workbook = XLSX.read(data);

  /* DO SOMETHING WITH workbook HERE */
}
document.getElementById("drop").addEventListener("drop", handleDrop, false);

Electron API

XLSX.readFile reads workbooks from the file system. showOpenDialog shows a Save As dialog and returns the selected file name. Unlike the Web APIs, the showOpenDialog flow can be initiated by app code:

/* from the renderer thread */
const electron = require('@electron/remote');

/* this function will show the open dialog and try to parse the workbook */
async function importFile() {
  /* show Save As dialog */
  const result = await electron.dialog.showOpenDialog({
    title: 'Select a file',
    filters: [{
      name: "Spreadsheets",
      extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
    }]
  });
  /* result.filePaths is an array of selected files */
  if(result.filePaths.length == 0) throw new Error("No file was selected!");
  // highlight-next-line
  return XLSX.readFile(result.filePaths[0]);
}

:::note

In older versions of Electron, showOpenDialog returned the path directly:

var dialog = require('electron').remote.dialog;

function importFile(workbook) {
  var result = dialog.showOpenDialog({ properties: ['openFile'] });
  return XLSX.readFile(result[0]);
}

:::

Electron Breaking Changes

The first version of this demo used Electron 1.7.5. The current demo includes the required changes for Electron 19.0.5.

There are no Electron-specific workarounds in the library, but Electron broke backwards compatibility multiple times. A summary of changes is noted below.

:::caution

Electron 6.x changed the dialog API. Methods like showSaveDialog originally returned an array of strings, but now returns a Promise. This change was not documented. Electron issue

Electron 9.0.0 and later require the preference nodeIntegration: true in order to require('xlsx') in the renderer process.

Electron 12.0.0 and later also require worldSafeExecuteJavascript: true and contextIsolation: true.

Electron 14+ must use @electron/remote instead of remote. An initialize call is required to enable Developer Tools in the window.

:::

Tauri

The NodeJS Module can be imported from JavaScript code.

This demo was tested against Tauri 1.0.5 on 2022 August 13.

:::note

Tauri currently does not provide the equivalent of NodeJS fs module. The raw @tauri-apps/api methods used in the examples are not expected to change.

:::

http and dialog must be explicitly allowed in tauri.conf.json:

    "allowlist": {
      "all": true,
      "http": {
        "all": true,
        "request": true,
        "scope": ["https://**"]
      },
      "dialog": {
        "all": true
      }

The "Complete Example" creates an app that looks like the screenshot:

SheetJS Tauri MacOS screenshot

Complete Example (click to show)
  1. Read Tauri "Getting Started" guide and install dependencies.

  2. Create a new Tauri app:

npm create tauri-app

When prompted:

  • App Name: SheetJSTauri
  • Window Title: SheetJS + Tauri
  • UI recipe: create-vite
  • Add "@tauri-apps/api": Y
  • ViteJS template: vue-ts
  1. Enter the directory:
cd SheetJSTauri

Open package.json with a text editor and add the highlighted lines:

{
  "name": "SheetJSTauri",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "tauri": "tauri"
  },
  "dependencies": {
// highlight-next-line
    "@tauri-apps/api": "^1.0.2",
    "vue": "^3.2.37",
// highlight-next-line
    "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz"
  },
  "devDependencies": {
// highlight-next-line
    "@tauri-apps/cli": "^1.0.5",
    "@vitejs/plugin-vue": "^3.0.3",
    "typescript": "^4.6.4",
    "vite": "^3.0.7",
    "vue-tsc": "^0.39.5"
  }
}
  1. Install dependencies:
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
  1. Enable operations by adding the highlighted lines to tauri.conf.json:
  "tauri": {
    "allowlist": {
// highlight-start
      "http": {
        "all": true,
        "request": true,
        "scope": ["https://**"]
      },
      "dialog": {
        "all": true
      },
// highlight-end
      "all": true
    }

In the same file, look for the "identifier" key and replace the value with com.sheetjs.tauri:

        "icons/icon.ico"
      ],
      // highlight-next-line
      "identifier": "com.sheetjs.tauri",
      "longDescription": "",
  1. Download App.vue and replace src/App.vue with the downloaded script.

  2. Build the app with

npm run tauri build

At the end, it will print the path to the generated program. Run the program!

Reading Files

There are two steps to reading files: obtaining a path and reading binary data:

import { read } from 'xlsx';
import { open } from '@tauri-apps/api/dialog';
import { readBinaryFile } from '@tauri-apps/api/fs';

const filters = [
  {name: "Excel Binary Workbook", extensions: ["xlsb"]},
  {name: "Excel Workbook", extensions: ["xlsx"]},
  {name: "Excel 97-2004 Workbook", extensions: ["xls"]},
  // ... other desired formats ...
];

async function openFile() {
  /* show open file dialog */
  const selected = await open({
    title: "Open Spreadsheet",
    multiple: false,
    directory: false,
    filters
  });

  /* read data into a Uint8Array */
  const d = await readBinaryFile(selected);

  /* parse with SheetJS */
  const wb = read(d);
  return wb;
}

Writing Files

There are two steps to writing files: obtaining a path and writing binary data:

import { write } from 'xlsx';
import { save } from '@tauri-apps/api/dialog';
import { writeBinaryFile } from '@tauri-apps/api/fs';

const filters = [
  {name: "Excel Binary Workbook", extensions: ["xlsb"]},
  {name: "Excel Workbook", extensions: ["xlsx"]},
  {name: "Excel 97-2004 Workbook", extensions: ["xls"]},
  // ... other desired formats ...
];

async function saveFile() {
  /* show save file dialog */
  const selected = await save({
    title: "Save to Spreadsheet",
    filters
  });

  /* Generate workbook */
  const bookType = selected.slice(selected.lastIndexOf(".") + 1);
  const d = write(wb, {type: "buffer", bookType});

  /* save data to file */
  await writeBinaryFile(selected, d);
}