In this chapter we’ll cover:
- How to implement a native open file dialog using Electron’s dialog module
- How to facilitate communication between the main process and a renderer process
- Why the main process should be used for interoperating with the operating system and file system
- How to expose functionality from the main process to renderer processes
- How to import that functionality from the main process into the renderer process using Electron’s
remote
module - How to send information from the main process to a renderer process using the
webContents
module - How to set up a listener for messages from the main process using the ipcRenderer module
Triggering Native File Dialogs
[code lang=javascript]
// Listing 4.1 Importing the dialog module (lib/main.js)
const { app, BrowserWindow, dialog } = require(‘electron’)
[/code]
Eventually the application should trigger our file opening functionality from multiple places. The first step is create a function that we’ll be able to reference later. Start by simply logging name of the file selected to the console once it has been selected.
[code lang=javascript]
// Listing 4.2 Creating an `showOpenFileDialog() function (lib/main.js)
const showOpenFileDialog = () => {
const files = dialog.showOpenDialog({
properties: [‘openFile’]
})
if (!files) { return }
console.log(files)
}
[/code]
[code lang=javascript]
// Listing 4.3 Invoking `showOpenFileDialog()` when the application is first ready
app.on(‘ready’, () => {
mainWindow = new BrowserWindow()
mainWindow.loadURL(`file://${__dirname}/index.html`)
showOpenFileDialog()
mainWindow.webContents.openDevTools()
mainWindow.on(‘closed’, () => {
mainWindow = null
})
})
[/code]
Reading Files Using Node
dialog.showOpenFileDialog()
returns an array consisting of the paths of the file or files that the user selected, but it does not read them on our behalf. Depending on what kind of file we’re building, we might want to handle opening the file differently. In this application, the contents of the file will be read and immediately displayed in the user interface. A different application that handled copying images or uploading them to an external service would take a contrasting approach when the user selected a file. Still another application could be adding a large movie to a playlist to watch later. In this case, it would be wasteful to immediately start opening the very large file at this time.
[code lang=javascript]
// Listing 4.4 Importing the Nodes fs module (lib/main.js)
const { app, BrowserWindow, dialog } = require(‘electron’)
const fs = require(‘fs’)
let mainWindow
app.on(‘ready’, () => {
mainWindow = new BrowserWindow()
mainWindow.loadURL(`file://${__dirname}/index.html`)
showOpenFileDialog()
mainWindow.webContents.openDevTools()
mainWindow.on(‘closed’, () => {
mainWindow = null
})
})
const showOpenFileDialog = () => {
const files = dialog.showOpenDialog({
properties: [‘openFile’]
})
if (!files) { return }
const file = files[0];
const content = fs.readFileSync(file).toString()
console.log(content)
}
[/code]
Scoping the File Open Dialog
Many desktop applications are able to limit the types of files that they allow the user to open. This is also true for applications built with Electron. Additional options can be added to the configuration object passed to dialog.showOpenDialog()
to restrict the dialog to file extensions that we’ve white listed.
[code lang=javascript]
// Listing 4.5 Whitelisting specific file types (lib/main.js)
const showOpenFileDialog = () => {
const files = dialog.showOpenDialog({
properties: [‘openFile’],
filters: [
{ name: ‘Text Files’, extensions:[‘txt’] },
{ name: ‘Markdown Files’, extensions: [‘md’, ‘markdown’] }
]
})
if (!files) { return }
const file = files[0]
const content = fs.readFileSync(file).toString()
console.log(content)
}
[/code]
Implementing Dialog Sheets in macOS
In macOS, we’re able to display dialog windows that drop down as sheets from the top of the window instead of being displayed in front of it. We can create this user interface easily in Electron by passing a reference to the BrowserWindow
instance—which we’ve stored in mainWindow
—as the first argument to dialog.showOpenDialog()
, before the configuration object.
[code lang=javascript]
const files = dialog.showOpenDialog(mainWindow, {
properties: [‘openFile’],
filters: [
{ name: ‘Text Files’, extensions: [‘txt’] },
{ name: ‘Markdown Files’, extensions: [‘md’, ‘markdown’] }
]
})
[/code]
Failitating Inter-Process Communication
In traditional web applications, we typically facilitate communication between the client- and server-side processes using a protocol like HTTP. With HTTP, the client can send a request with information, the server receives this request, handles it appropriately, and sends a response to the client.
In Electron applications, things are a little different. As we’ve discussed in the previous chapters, Electron applications consist of multiple processes: one main process and one or more render processes. Everything is running on our computer, but there is a similar separation of roles to the client-server model. We don’t use HTTP to communicate between processes. Instead Electron provides several modules for coordinating communication between the main and renderer processes.
Our main process is in charge of interfacing with the native operating system APIs. It’s in charge of spawning renderer processes, defining application menus, displaying open and save dialogs, registering global shortcuts, requesting power information from the OS, and more. Electron enforces this by making many of the modules needed to perform these tasks available only in the main process.
Electron only provides a subset of its modules to each process doesn’t keep us from accessing Node APIs that are separate from Electron’s modules. We can access a database or the file system from renderer process if we want, but there are some compelling reasons to keep this kind of functionality in the main process. We could potentially have many renderer processes, but we will always only have one main process. Reading from and writing to the file system from one of our renderer processes could become problematic because we could end up in a situation where one or more processes are trying to write to the same file at the same time or read from a file while another renderer process is overwriting it.
A given process in JavaScript executes our code on a single thread and can only do one thing at a time. By delegating these tasks to the main process, we can be confident that only one process is performing reading or writing to a given file or database at a time. Other tasks will follow the normal JavaScript protocol of patiently waiting on the event queue until the main process is done with its current task.
More recently, protocols like WebSockets and WebRTC have emerged that allow for two- way communication between the client and server and even communication between clients without needing a central server to facilitate communication. When we’re building desktop applications, we typically won’t be using HTTP or WebSockets, but Electron has several ways to coordinate inter-process communication, which we’ll begin to explore in this chapter.
As you might have guessed, this will require us to coordinate between the renderer process where the button was clicked and the main process, which is responsible for displaying the dialog and reading the chosen file from the file system. After reading the file, main process needs to send the contents of the file back over to the renderer process in order to be displayed and rendered in the left and right panes respectively.