Chapter 4. Using native file dialog boxes and facilitating interprocess communication

This chapter covers

  • Implementing a native open file dialog box using Electron’s dialog module
  • Facilitating communication between the main process and a renderer process
  • Exposing functionality from the main process to renderer processes
  • Importing functionality from the main process into the renderer process using Electron’s remote module
  • Sending information from the main process to a renderer process using the webContents module and setting up a listener for messages from the main process using the ipcRenderer module

In the previous chapter, we laid the foundation for our first Electron project, a notes application that takes Markdown from the left pane and renders it as HTML in the right pane. We set up our main process and configured it to spawn a renderer. We set up package.json, installed the necessary dependencies, created the main and renderer processes, and laid out the UI. We also explored ways we can make our application feel like a desktop application, but we haven’t added a feature that is far outside the scope of what a traditional web application could do yet.

Right now, the application allows the user to write in the Markdown view. When the user presses a key in the Markdown view, the application automatically renders the Markdown to HTML and displays it in the HTML view.

In this chapter, we’ll add the ability to trigger a native file dialog box and select a text file from anywhere on the filesystem and load it into our application. By the end of the chapter, the Open File button in the renderer process’s browser window will trigger the Open File dialog box from the main process. Before we can do that, it’s important to discuss how to communicate between processes in a bit more depth. We start on the chapter-3 branch, which can be found at http://mng.bz/11Kd. The code at the end of the chapter can be found at http://mng.bz/0C34. Alternatively, you can pull down the master branch and check out either of these two branches.

git clone  https://github.com/electron-in-action/firesale.git chapter-3
git checkout -f chapter3

4.1. Triggering native file dialog boxes

An easy way to get started is to prompt the user for a file to open when the application first starts and emits its ready event, as shown in figure 4.1. Our application is already listening for the ready event before we create our BrowserWindow instance. Later in this chapter, we learn how to trigger this functionality from the UI. In the next chapter, we learn how to trigger it from the application menu as well.

Figure 4.1. Our application will trigger the Open File dialog box when it starts. By the end of the chapter, this functionality will be replaced by the ability to trigger the dialog box from the UI.

You create native dialogs using Electron’s dialog module. Add the code in listing 4.1 to app/main.js just beneath where the other Electron modules are required.

Listing 4.1. Importing the dialog module: ./app/main.js
const { app, BrowserWindow, dialog } = require('electron');

Eventually the application should trigger our file-opening functionality from multiple places. The first step is to create a function to reference later. Start by logging the name of the file selected to the console after it has been selected.

Listing 4.2. Creating a getFileFromUser() function: ./app/main.js
const getFileFromUser = () => {
  const files = dialog.showOpenDialog({     1
    properties: ['openFile']                2
  });

  if (!files) { return; }                   3

  console.log(files);                       4
};

  • 1 Triggers the operating system’s Open File dialog box. We also pass it a JavaScript object of different configuration arguments to the function.
  • 2 The configuration object sets different properties on the Open File dialog.
  • 3 If we don’t have any files, return early from the function.
  • 4 Logs the files to the console

Our getFileFromUser() function is a wrapper over dialog.showOpenDialog() that we can use in multiple places in our application without having to repeat ourselves. It will trigger the showOpenDialog() method on dialog and pass it a JavaScript object with different settings that we can adjust as needed. In JavaScript, an object’s keys are called its properties. The properties of the object passed to dialog.showOpen-Dialog() configure certain characteristics of the dialog box we’re creating. One such setting is the properties of the dialog box itself. The properties property on the configuration object takes an array of different flags we can set on the dialog box. In this case, we’re activating only the openFile flag, which signifies that this dialog box is for selecting a file to open—as opposed to selecting a directory or multiple files. The other flags available are openDirectory and multiselections.

dialog.showOpenDialog() returns the names of the files selected. An array of the paths selected by the user are stored in a variable called files. If the user presses cancel, dialog.showOpenDialog() returns undefined and breaks if we try to call any methods on files while it’s undefined. The return statement guards against that by leaving the function early if files is a false value—and undefined is, in fact, a false value.

getFileFromUser() must be called somewhere in our application to trigger the dialog box. Eventually, it will be called from the UI and the application menu. A convenient place to do this—for now—is when the application starts. Call getFileFrom-User() when the app module fires its ready event, as shown in the following listing. This step will be removed when our UI is configured to trigger getFileFromUser() from the renderer process.

Listing 4.3. Invoking getFileFromUser() when the application is first ready
app.on('ready', () => {
  mainWindow = new BrowserWindow({ show: false });

  mainWindow.loadFile('index.html');

  mainWindow.once('ready-to-show', () => {
    mainWindow.show();
    getFileFromUser();                        1
  });

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

  • 1 We’ll call getFileFromUser() when the window is ready to show. getFileFromUser() is defined in listing 4.2.

When our application starts and the window is fully loaded, users immediately will see a File dialog box, which will allow them to select a file (see figure 4.2). We eventually remove this function call from the launch process and assign it to the Open File button in the UI.

Figure 4.2. Electron is able to trigger native file dialog boxes in each of its supported operating systems.

In figure 4.3, we can see the results of our selection in the Open File dialog box displayed in our terminal. Notice that dialog.showOpenDialog() returns an array. If multiselections is activated in the dialog’s properties array, the user can select multiple files. Electron always returns an array for consistency.

Figure 4.3. Upon selecting a file, the full path of the file is logged to the console in our terminal window.

4.2. Reading files using Node

dialog.showOpenDialog() 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 are read and immediately displayed in the UI. A different application that handles copying images or uploads them to an external service might take a contrasting approach when the user selects a file. Still another application might add a large movie to a playlist to watch later. In this case, it would be wasteful to immediately start opening the large file.

Node comes with a set of tools for working with files in its standard library. The built-in fs library handles common filesystem operations such as reading and writing files, so you should require it near the top of app/main.js.

Listing 4.4. Importing Node’s fs module: ./app/main.js
const { app, BrowserWindow, dialog } = require('electron');
const fs = require('fs');                                    1

app.on('ready',() => { ... });                               2

const getFileFromUser = () => {
  const files = dialog.showOpenDialog(mainWindow, {
    properties: ['openFile']
  });

  if (!files) { return; }

  const file = files[0];                                     3
  const content = fs.readFileSync(file).toString();          4

  console.log(content);
};

  • 1 Requires Node’s fs library.
  • 2 Code omitted for clarity.
  • 3 Pulls the first file out of the array
  • 4 Reads from the file, and converts the resulting buffer to a string.

In listing 4.4, the application opens only one file at a time. files[0] selects the first—and only—file path out of the array from dialog.showOpenDialog(). In fs.read-FileSync(file) the file path is passed as an argument to fs.readFileSync(). Node doesn’t know what kind of file was opened, so fs.readFileSync() returns a buffer object. We know, however, that we typically work with plain text in this particular application. We convert it to a string and log the contents of the file to the terminal, as shown in figure 4.4.

Figure 4.4. The contents of the file are logged to the user’s terminal.

4.2.1. Scoping the Open File dialog

As you can see in figure 4.4, getFileFromUser() successfully logs the contents of a text file to the terminal. But there is a problem. By default, dialog.showOpenDialog() lets us open any file on our computer, with no consideration for what types of files we’re prepared to handle. Figure 4.5 shows the problematic result when we open an image file instead of a text file through the dialog box.

Figure 4.5. If the user selects a nontext file, the function logs the binary data.

Many desktop applications can limit the file types that the users can open. This is also true for applications built with Electron. Our application isn’t suited for opening music files, so we should probably not let the user select MP3s. Additional options can be added to the configuration object passed to dialog.showOpenDialog() to restrict the dialog box to file extensions that we’ve whitelisted.

Listing 4.5. Whitelisting specific file types: ./app/main.js
const getFileFromUser = () => {
  const files = dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [                                                     1
      { 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);
};

  • 1 The filters property allows us to specify what types of files our application should be able to open and disables any file that doesn’t match our criteria.

In the listing we added a second property to the object passed to dialog.showOpenDialog(). In Windows, the dialog displays the name Markdown Files in the drop-down menu, as seen in figure 4.6. In macOS, there is no drop-down menu, but we cannot select images that do not have one of the extensions, as shown in figure 4.7.

Figure 4.6. In Windows, we can switch between different types of files.

Figure 4.7. macOS does not support switching between types of files but does allow us to select any file that is eligible as defined by the filters option.

4.2.2. Implementing dialog sheets in macOS

Electron applications are designed to be cross-platform, meaning they work on macOS, Windows, and Linux. Electron provides interfaces to native features and APIs that exist in each of the supporting operating systems but do not exist in the others. We saw this earlier when we provided a name for our file extension filters. This name appears in Windows, but macOS does not have this capability. Electron takes advantage of this feature if it is available, but it still works in the cases where it isn’t.

In macOS, we’re able to display dialog boxes that drop down as sheets from the top of the window instead of being displayed in front of it (listing 4.6). We can create this UI 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.

Listing 4.6. Creating sheet dialogs in macOS: ./app/main.js
const getFileFromUser = () => {
  const files = dialog.showOpenDialog(mainWindow, {      1
    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);
};

  • 1 Passing a reference to a BrowserWindow instance to dialog.showOpenDialog will cause macOS to display the dialog box as a sheet coming down from the title bar of the window. It has no effect on Windows and Linux.

With this simple change, Electron now displays the Open File dialog as a sheet that drops down from the window passed to the method, as shown in figure 4.8.

Figure 4.8. Instead of appearing as an additional window in front of our application’s window, the Open File dialog box now drops down from the menu’s title bar in macOS.

4.3. Facilitating interprocess communication

We’ve written all of the code for selecting files and reading files in our main process. But how do we send the contents of the file to the renderer process? How do we trigger the getFileFromUser() function in our main process from our UI?

We have to deal with similar issues when building traditional web applications. It’s not exactly the same because all of the code runs on the client’s computer, but thinking about how we usually build web applications can serve as a helpful metaphor for understanding how to structure our Electron applications. See figure 4.9.

Figure 4.9. The division of responsibilities in Electron applications versus traditional web applications.

On the web, we typically write code that runs in one of two places: on our servers or client-side code that runs in our users’ browsers. The client-side code is what renders the UI. It listens for and handles user actions and updates the UI to display the current state of the application. There are, however, limits to what we can do with client-side code. As we discussed in chapter 1, we cannot read from or write to the database or filesystem. Server-side code runs on our computer. It has access to the database. It can write to the log files on our system.

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 renderer processes. Everything runs 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 dialog boxes, 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, as shown in figure 4.10.

Figure 4.10. Electron provides different modules to the main and renderer processes. These modules represent the code capabilities of Electron. This list is likely to grow and may be incomplete by the time you read this. I encourage you to visit the documentation to see the latest and greatest features.

Electron provides only a subset of its modules to each process and doesn’t keep us from accessing Node APIs that are separate from Electron’s modules. We can access a database or the filesystem from the renderer process if we want, but there are compelling reasons to keep this kind of functionality in the main process. We could potentially have many renderer processes, but we will always have only one main process. Reading from and writing to the filesystem from one of our renderer processes could become problematic; we could end up in a situation where one or more processes try 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 do only 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 follow the normal JavaScript protocol of patiently waiting in the event queue until the main process is done with its current task.

It makes sense that the main process handles tasks that call native operating system APIs or provides filesystem access, but the UI that likely triggers these operations is called in the renderer process. Even though all of the code is running on the same computer, we still have to coordinate the communication between our processes, just as we would have to coordinate communication between the client and server.

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 interprocess communication, which we begin to explore in this chapter and is shown in figure 4.11.

Figure 4.11. Implementing the Open File button involves coordinating communication between the renderer process and the main process.

Our UI contains a button with the label Open File. When the user clicks this button, our application should provide a dialog box allowing the user to select a file to open. After the user selects a file, our application should read the contents of the file, display them in the left pane of our application, and render the corresponding HTML in the right pane.

As you might have guessed, this requires 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 filesystem. After reading the file, the main process needs to send the contents of the file back over to the renderer process (next listing) to be displayed and rendered in the left and right panes, respectively.

Listing 4.7. Adding an event listener in the renderer process: ./app/renderer.js
const marked = require('marked');

const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default');

const renderMarkdownToHtml = (markdown) => {
  htmlView.innerHTML = marked(markdown, { sanitize: true });
};

markdownView.addEventListener('keyup', (event) => {
  const currentContent = event.target.value;
  renderMarkdownToHtml(currentContent);
});

openFileButton.addEventListener('click', () => {       1
  alert('You clicked the "Open File" button.');
});

  • 1 Opts in to an updated CSS box model that will correctly set the width and height of elements

Start by adding an event listener to the Open File button in our renderer process. With our event listener in place, it’s time to coordinate with the main process to trigger the Open File dialog box we created earlier.

4.3.1. Introducing the remote module

Electron provides numerous ways to facilitate interprocess communication. The first one is the remote module—a simple way to perform interprocess communication from the renderer process to the main process. The remote module, available only in the renderer process, works as a proxy to the main process by mirroring the modules that are accessible in the main process. The remote module also takes care of communication to and from the main process when we access any of those properties.

Depicted in figure 4.12, the remote module has several properties that overlap with the modules available only to the main process. In our renderer process, we can require the remote module, and it provides access to objects and properties in the main process, as shown in figure 4.13.

Figure 4.12. The remote module shares many of the same properties as the Electron module in the main process.

Figure 4.13. The remote module provides access to modules normally available only to the main process.

When we call a method or property on the remote object, it sends a synchronous message to the main process, executes in the main process, and sends a message back to the renderer process with the results. The remote module allows us to define functionality in the main process and easily makes it available to our renderer processes.

4.4. Triggering the Open File function using interprocess communication

The application can now trigger an Open File dialog box and read the contents of the file that the user selected in the main process. We’ve also added an event listener to the Open File button in the renderer process. Now it’s just a matter of connecting them using the interprocess communication techniques we explored earlier.

4.4.1. Understanding the CommonJS require system

To use functionality from the main process using the remote module, we need to take advantage of Node’s CommonJS module system to expose that functionality to other files in our application. We’ve used require in this book to pull in functionality from Electron, the Node standard library, and third-party libraries, but this is the first time we use it with our own code. Let’s spend a few minutes reviewing how it works.

Node’s module system consists of two primary mechanisms: the ability to require functionality from other sources, and the ability to export functionality to be consumed by other sources. When we require code from other sources, the other source could be a file we’ve written, a third-party module, a module from the Node, or a module provided by Electron. We’ve used Node’s built-in require function at the top of both our main and renderer processes.

When we require a module, what exactly are we importing? In Node, we explicitly declare what functionality should be exported from the module, as shown in listing 4.8. This function is imported in listing 4.9. Every module in Node has a built-in object called exports that starts out as an empty object. Anything we add to the exports object is available when we require it from another file.

Listing 4.8. Exporting a function in Node: basic-math.js
exports.addTwo = n => n + 2;
Listing 4.9. Importing a function in Node
const basicMath = require('./basic-math');

basicMath.addTwo(4); // returns 6

4.4.2. Requiring functionality from another process

The built-in require function does not work across processes. When we’re working in the renderer process, any functionality we use from the built-in require function to import will be part of the renderer process. When we’re working in the main process, any functionality we require will be part of the main process. But what happens when we are in the renderer process and we want to require functionality from the main process?

Electron’s remote module has its own require method that allows it to require functionality from the main process in our renderer process. Using remote.require returns a proxy object—like the other properties on the remote object. Electron takes care of all of the interprocess communication on our behalf.

To implement the functionality we set out at the beginning of this chapter, the main process must export its getFileFromUser() function so that we can import it into our renderer code. This listing updates a single line in app/main.js.

Listing 4.10. Exporting ability to open the file dialog from the renderer process: ./app/main.js
const getFileFromUser = exports.getFileFromUser  = () => {       1
  const files = dialog.showOpenDialog(mainWindow, {
    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);
};

  • 1 In addition to creating a constant in this file, we assign it as a property of the exports object, which will be accessible from other files—specifically, the renderer process.

The code takes the getFileFromUser() function we created and exports it as a property with the same name on the exports object. The render process needs to bring in Electron’s remote module and then use the remote.require function to get a reference to the getFileFromUser() function from the main process in our renderer process. This is different from the built-in require function shown in listing 4.11 because the imported code is evaluated in terms of the main process, not the renderer process in which it was required. This is accomplished in four steps:

  1. Require Electron in our renderer process.
  2. Store a reference to the remote module.
  3. Use remote.require to require the main process.
  4. Store a reference to the getFileFromUser() function exported from the main process.
Listing 4.11. Requiring functions from the main process in the renderer process: ./app/renderer.js
const { remote } = require('electron');
const mainProcess = remote.require('./main.js');

We can now call the getFileFromUser() function we exported from the main process in our renderer process. Let’s replace the functionality in our event listener to trigger the Open File dialog box instead of firing an alert.

Listing 4.12. Triggering getFileFromUser() in the main process from the UI: ./app/renderer.js
openFileButton.addEventListener('click', () => {
  mainProcess.getFileFromUser();
});

If we start our Electron application and click the Open File button, it correctly triggers the Open File dialog box. With that in place, we’re still logging the files only to the console in the main process. To complete our feature, the main process must send the file’s contents back to the renderer process to be displayed in our application.

4.5. Sending content from the main process to the renderer process

The remote module facilitates access to functionality from the main process in our renderer processes, but it doesn’t allow for the inverse. To send the contents of the file that the user selected back to the renderer process to be rendered in the UI, we need to learn another technique for communicating between processes.

Each BrowserWindow instance has a property called webContents, which stores an object responsible for the web browser window that we create when we call new Browser-Window(). webContents is similar to app because it emits events based on the lifecycle of the web page in the renderer process.

The following is an incomplete list of some of the events that you can listen for on the webContents object:

  • did-start-loading
  • did-stop-loading
  • dom-ready
  • blur
  • focus
  • resize
  • enter-full-screen
  • leave-full-screen

webContents also has a number of methods that can trigger different functions in the renderer process from the main process. In the previous chapter, we opened the Chrome Developer Tools in the renderer process from the main process using mainWindow.webContents.openDevTools(). mainWindow.loadURL(’file://${__dirname}/index.html’), an alias for mainWindow.webContents.loadURL(), loaded our HTML file into the renderer process when the application first launched. Figure 4.14 shows more aliases.

Figure 4.14. BrowserWindow instances have methods that are aliases to Electron’s webContents API.

webContents has a method called send() which sends information from the main process to a renderer process. webContents.send() takes a variable number of arguments. The first argument, which is an arbitrary string, is the name of the channel on which to send the message. An event listener in the renderer process listens on the same channel. This flow will become clearer when we see it in action. All of the subsequent arguments after the first are passed along to the renderer process.

4.5.1. Sending the file contents to the renderer contents

Our current implementation reads the file that the user selected and logs it to the terminal. mainWindow.webContents.send() sends the contents of the file to the renderer process instead. The next chapter covers additional ways to open files that do not require a dialog box prompting the user to select a particular file because we do encounter situations where we will want to open a file without triggering the dialog box.

Listing 4.13. Sending content from the main to a renderer process: ./app/main.js
const getFileFromUser = exports.getFileFromUser =  () => {
  const files = dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [
      { name: 'Text Files', extensions: ['txt'] },
      { name: 'Markdown Files', extensions: ['md', 'markdown'] }
    ]
  });

  if (files) { openFile(files[0]); }                             1
};

const openFile = (file) => {
  const content = fs.readFileSync(file).toString();
  mainWindow.webContents.send('file-opened', file, content);     2
};

  • 1 Previously, we interrupted the function with a return statement in the event that files were undefined. In this example, we’ll flip that logic and pass the first file to Open File when dialog.showOpenFile() successfully returns an array of file paths.
  • 2 We’ll send the name of the file and its content to the renderer process over the “file-opened” channel.

The main process is now broadcasting the name of the file and its contents over the file-opened channel. The next step is to set up a listener on the file-opened channel in the renderer process using the ipcRenderer module. Electron comes with two basic modules for sending messages back and forth between processes: ipcRenderer and ipcMain. Each module is available only in the process type with which it shares a name.

ipcRenderer can send messages to the main process. More important to our immediate needs, it can also listen for messages that were sent from the main process using webContents.send(). It requires the ipcRenderer module in the renderer process.

Listing 4.14. Importing the ipcRenderer module: ./app/renderer.js
const { remote, ipcRenderer } = require('electron');          1
const mainProcess = remote.require('./main.js');

  • 1 We’ll import the ipcRenderer module in our renderer process.

With that in place, we can now set up a listener. ipcRenderer listens on the file-opened channel, adds the content to the page, and renders the Markdown as HTML.

Listing 4.15. Listening for messages on the file-opened channel: ./app/renderer.js
ipcRenderer.on('file-opened', (event, file, content) => {
  markdownView.value = content;
  renderMarkdownToHtml(content);
});

ipcRenderer.on() takes two arguments: the channel to listen on and a callback function that defines an action to take when the renderer process receives a message on the channel on which you’re setting up the listener. The callback function is provided with a few arguments when it is called. The first is an event object, which is just like a normal event listener in the browser. It contains information about the event for which we set up the listener. The additional arguments are what were provided when using webContents.send() in the main process. In listing 4.13, we sent the name of the file and its contents. Those will be additional arguments passed to our listener.

With these new additions, the user can now click the Open File button, select a file using a native file dialog box, and render the contents in the UI. We’ve successfully implemented the feature that we set out to implement at the beginning of the chapter. The code for our main and renderer processes should look something like the following two listings.

Listing 4.16. Open File functionality implemented in the main process: ./app/main.js
const { app, BrowserWindow, dialog } = require('electron');
const fs = require('fs');

let mainWindow = null;

app.on('ready', () => {
  mainWindow = new BrowserWindow({ show: false });

  mainWindow.loadFile('index.html');

  mainWindow.once('ready-to-show', () => {
    mainWindow.show();
  });

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

const getFileFromUser = exports.getFileFromUser = () => {
  const files = dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [
      { name: 'Text Files', extensions: ['txt'] },
      { name: 'Markdown Files', extensions: ['md', 'markdown'] }
    ]
  });

  if (files) { openFile(files[0]) }
};

const openFile = (file) => {
  const content = fs.readFileSync(file).toString();
  mainWindow.webContents.send('file-opened', file, content);
};
Listing 4.17. Open File functionality implemented: ./app/renderer.js
const { remote, ipcRenderer } = require('electron');
const mainProcess = remote.require('./main.js');

const marked = require('marked');

const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default');

const renderMarkdownToHtml = (markdown) => {
  htmlView.innerHTML = marked(markdown, { sanitize: true });
};

markdownView.addEventListener('keyup', (event) => {
  const currentContent = event.target.value;
  renderMarkdownToHtml(currentContent);
});

openFileButton.addEventListener('click', () => {
  mainProcess.getFileFromUser();
});

ipcRenderer.on('file-opened', (event, file, content) => {
  markdownView.value = content;
  renderMarkdownToHtml(content);
});

Summary

  • Electron provides the dialog module for creating a variety of native operating system dialogs.
  • Open dialog boxes can be configured to allow for a single file or directory as well as multiple files or directories.
  • Open dialog boxes can be configured to allow the user to select only certain file types.
  • Open dialog boxes return an array consisting of the one or more files or directories selected by the user.
  • Electron does not include an ability to read files. Instead, we use Node’s fs module to read from and write to the filesystem.
  • Each operating system offers a different set of features. Electron uses the features available while providing a graceful fallback if that feature does not exist in a given operating system.
  • In macOS, we can have a dialog box drop down as a sheet from one of the windows by providing a reference to that window as the first argument in dialog.showOpenDialog().
  • Native operating system APIs and filesystem access should be handled by the main process, while rendering the UI and responding to user input should be handled by the renderer process.
  • Electron provides a different set of modules to the main process and renderer processes.
  • Electron provides a number of mechanisms for communicating between processes.
  • The remote module provides a proxy to the main process modules and functions and makes that functionality available in our renderer processes.
  • We can send messages from the main process to a renderer process using webContents.send().
  • We can listen for messages sent from the main processes in our renderer processes using the ipcRenderer module.
  • We can namespace messages using channels, which are arbitrary strings. In this chapter, we used the file-opened channel to send and listen for messages.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.120.133