docs.sheetjs.com/docz/docs/03-demos/19-desktop/01-electron.md

15 KiB

title sidebar_label pagination_prev pagination_next sidebar_position sidebar_custom_props
Electrified Sheets with Electron Electron demos/mobile/index demos/cli/index 1
summary
Embedded NodeJS + Chromium

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

Electron is a modern toolkit for building desktop apps. Electron apps use the same technologies powering Chromium and NodeJS.

SheetJS is a JavaScript library for reading and writing data from spreadsheets.

The "Complete Example" section covers a complete desktop app to read and write workbooks. The app will look like the screenshots below:

Windows macOS Linux

Windows screenshot

macOS screenshot

Linux screenshot

Integration Details

Electron uses a multi-process architecture, with the main process handling system level operations and I/O, and the renderer process handling UI and web content.

Renderer Process Limitations

The renderer process is sandboxed and cannot run any non-browser code.

Main Process Limitations

The main process can run any NodeJS code, but it cannot access the DOM or any browser APIs.

To allow communication between the main and renderer processes, Electron recommends building a context bridge to expose low-level system calls and NodeJS APIs to the renderer process.

// preload.js
const { contextBridge, ipcRenderer, shell } = require('electron');
// import nodejs modules we wish to expose APIs from.
const path = require('path');
const XLSX = require('xlsx');

// The contextBridge API allows us to expose APIs to the renderer process.
contextBridge.exposeInMainWorld('SheetJSDemoAPI', {
  // request OS file dialogs from the main process
  openFile:  (filters) => ipcRenderer.invoke('dialog:openFile', filters),
  saveFile:  (filters) => ipcRenderer.invoke('dialog:saveFile', filters),
  message:   (msg)    => ipcRenderer.invoke('dialog:message',  msg),
  // open external links in the default browser
  openExternal: (url) => shell.openExternal(url),
  // listen for file open events from the main process
  onFileOpened: (cb) => ipcRenderer.on('file-opened', (_e, fp) => cb(fp)),

  // You can use this to expose nodejs APIs to the renderer process.
  basename: (p) => path.basename(p),
  extname: (p) => path.extname(p),

  // Here for example we are exposing the sheetjs package to the renderer process. 
  xlsx: XLSX,
});

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

In the demo the drag and drop snippet applies to the entire window via the document.body element. However it can easily be applied to any element 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.

We can now use the exposed APIs from our preload script above to show the open dialog and try to parse the workbook from within the renderer process.

// index.js - renderer process

// our exposed bridge APIs are available as SheetJSDemoAPI on the window object
const openFile = SheetJSDemoAPI.openFile; // request the open file dialog from the main process
// We can also access the SheetJS package from the exposed bridge APIs
const XLSX = SheetJSDemoAPI.xlsx;

/* this function will show the open dialog and try to parse the workbook */
async function importFile() {
  /* show open file dialog */
  const result = await openFile([{
      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]);
}

The actual implementation of the openFile function is handled within the main process in main.js.

// main.js - main process
const { ipcMain, dialog } = require('electron');

ipcMain.handle('dialog:openFile', (_e, filters) =>
  dialog.showOpenDialog({ title: 'Select a file', filters, properties: ['openFile'] })
);

Writing Files

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

The implementation for saving files looks very similar to the one above thanks to our bridge API.

// index.js - renderer process
// our exposed bridge APIs are available as SheetJSDemoAPI on the window object
const saveFile = window.SheetJSDemoAPI.saveFile; // request the save file dialog from the main process
const XLSX = window.SheetJSDemoAPI.xlsx;

async function exportFile(workbook) {
  const result = await saveFile([{
      name: "Spreadsheets",
      extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
  }]);
  if(result.filePaths.length == 0) throw new Error("No file was selected!");
  XLSX.writeFile(workbook, result.filePaths[0]);
}

And here is the implementation of the saveFile function in main.js:

// main.js - main process
const { ipcMain, dialog } = require('electron');

ipcMain.handle('dialog:saveFile', (_e, filters) =>
  dialog.showSaveDialog({ title: 'Save file as', filters })
);

Complete Example

:::note Tested Deployments

This demo was tested in the following environments:

OS and Version Architecture Electron Date
macOS 15.3 darwin-x64 35.1.2 2025-03-31
macOS 14.5 darwin-arm 35.1.2 2025-03-30
Windows 11 win11-x64 33.2.1 2025-02-11
Windows 11 win11-arm 33.2.1 2025-02-23
Linux (HoloOS) linux-x64 33.2.1 2025-01-02
Linux (Debian) linux-arm 33.2.1 2025-02-16

:::

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

You can also use electron-builder to build a packaged installer binary.

  1. Download the demo files:

:::caution pass

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.

:::

These instructions can be run in a Terminal (bash) or Command Prompt window:

mkdir sheetjs-electron
cd sheetjs-electron
curl -LO https://docs.sheetjs.com/electron/package.json
curl -LO https://docs.sheetjs.com/electron/main.js
curl -LO https://docs.sheetjs.com/electron/index.html
curl -LO https://docs.sheetjs.com/electron/index.js
curl -LO https://docs.sheetjs.com/electron/preload.js
curl -LO https://docs.sheetjs.com/electron/styles.css

:::note pass

In PowerShell, the command may fail with a parameter error:

Invoke-WebRequest : A parameter cannot be found that matches parameter name 'LO'.

curl.exe must be invoked directly:

curl.exe -LO https://docs.sheetjs.com/electron/package.json
curl.exe -LO https://docs.sheetjs.com/electron/main.js
curl.exe -LO https://docs.sheetjs.com/electron/index.html
curl.exe -LO https://docs.sheetjs.com/electron/index.js
curl.exe -LO https://docs.sheetjs.com/electron/preload.js
curl.exe -LO https://docs.sheetjs.com/electron/styles.css

:::

  1. Install dependencies:
npm install
  1. To verify the app works, run in the test environment:
npx -y electron .

The app will run.

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

or

npm run dist

if you want to generate an installer binary.

This will create a package in the out\make folder and a standalone binary, or an installer binary in /dist if you used npm run dist.

:::caution pass

On Linux, the packaging step may require additional dependencies1

:::

:::info pass

When the demo was last tested on Windows ARM, the generated binary targeted x64. The program will run on ARM64 Windows.

:::

Working with OS level file open events.

The demo has been preconfigured to handle OS level file open events, such as the "open with" context menu or open CLI command for all file types SheetJS supports. In order to pre-register your application as a handler for any other file types, it is necessary to modify the package.json file as such.

// ...existing content
 "build": {
    "appId": "com.sheetjs.electron",
    "fileAssociations": [
      {
        "ext": [ // supported extensions to register with the OS.
          "xls","xlsx","xlsm","xlsb","xml","csv","txt","dif",
          "sylk","slk","prn","ods","fods","htm","html","numbers"
        ],
        "name": "Spreadsheet / Delimited File",
        "description": "Spreadsheets and delimited text files opened by SheetJS-Electron",
        "role": "Editor"
      }
    ],
    "mac":   { "target": "dmg"  },
    "win":   { "target": "nsis" },
    "linux": { "target": "deb"  }
  },

this snippet makes it possible to generate installers for MacOS, Windows and Linux which will automatically register the application as a handler for the specified file types.

:::info pass

It is also possible to open files using the "open with" context menu without registering the application as a handler for the specified file types. This however, requires manually selecting the application binary as a target to open the file with.

:::

Testing

  1. Download the test file pres.numbers

  2. Launch the generated application:

Architecture Command
darwin-x64 open ./out/sheetjs-electron-darwin-x64/sheetjs-electron.app
darwin-arm open ./out/sheetjs-electron-darwin-arm64/sheetjs-electron.app
win11-x64 .\out\sheetjs-electron-win32-x64\sheetjs-electron.exe
win11-arm .\out\sheetjs-electron-win32-x64\sheetjs-electron.exe
linux-x64 ./out/sheetjs-electron-linux-x64/sheetjs-electron
linux-arm ./out/sheetjs-electron-linux-arm64/sheetjs-electron

Electron API

  1. Click "Click here to select a file from your computer". With the file picker, navigate to the Downloads folder and select pres.numbers.

The application should show data in a table.

  1. Click "Export" and click "Save" in the popup. By default, it will try to write to Untitled.xls in the Downloads folder.

:::note pass

In some tests, the dialog did not have a default name.

If there is no default name, enter Untitled.xls and click "Save".

:::

The app will show a popup once the data is exported. Open the file in a spreadsheet editor and compare the data to the table shown in the application.

Open with menu

  1. Close the application, end the terminal process and re-launch (see step 6)

  2. Open the Downloads folder in a file explorer or finder window.

  3. Right-click the pres.numbers file and select "Open with".

  4. Select your application binary by navigating to the folder where the application was built (see step 4).

The application should show data in a table.

Drag and Drop

  1. Close the application, end the terminal process and re-launch (see step 6)

  2. Open the Downloads folder in a file explorer or finder window.

  3. Click and drag the pres.numbers file from the Downloads folder to the bordered "Drop a spreadsheet file" box. The file data should be displayed.

File Input Element

  1. Close the application, end the terminal process and re-launch (see step 6)

  2. Click "Choose File". With the file picker, navigate to the Downloads folder and select pres.numbers.

Electron Breaking Changes

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

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

:::caution pass

Electron 6 changed the return types of dialog API methods. The old dialog methods have been renamed:

Electron 1 - 5 Electron 6
showOpenDialog showOpenDialogSync
showSaveDialog showSaveDialogSync

This change was not properly documented!

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

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

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

For demos built on top of Electron 36 and later we isolate the processes entirely and the demo no longer requires @electron/remote. However, nodeIntegration: false by default now means that the renderer process no longer has access to NodeJS APIs. To expose NodeJS APIs to the renderer process, we use the contextBridge API to expose APIs from the main process to the renderer process. See more. This has been best practice since Electron 25. :::


  1. See "Makers" in the Electron Forge documentation. On Linux, the demo generates rpm and deb distributables. On Arch Linux and the Steam Deck, sudo pacman -Syu rpm-tools dpkg fakeroot installed required packages. On Debian and Ubuntu, sudo apt-get install rpm sufficed. ↩︎