Our bookmark manager was a fine place to start, but it only scratches the surface of what we can do with Electron. In this chapter, we dig a little bit deeper and lay the foundation for an application with stronger ties to the user’s operating system. Over the course of the next few chapters, we’ll implement features that trigger the operating system’s GUIs, read from and write to the filesystem, and access the clipboard.
We are building a simple note editor that allows us to create new or open existing Markdown files, convert them to HTML, and save the HTML to the filesystem and clipboard. Let’s call the application Fire Sale as an only slightly clever play on price markdowns—because it’s a Markdown editor after all. At the end of the chapter, we’ll discuss the techniques and tools available for debugging our Electron applications when things go awry.
Let’s start by setting goals for our humble, little application. Many of our features might seem a bit banal for a desktop application, and that’s the point. They’re standard fare for a desktop application but completely outside of the realm of abilities for traditional web applications, which cannot access anything outside of their isolated browser tab. Our application will consist of two panes: a left pane where the user can write or edit Markdown and a right pane that displays the user’s Markdown rendered as HTML. Along the top we have a series of buttons, which will allow the user to load a text file from the filesystem as well as write the result to the clipboard or filesystem.
In the first phase of our application, we build a UI based on the wireframe in figure 3.1. We can also add additional UI elements to the wireframe—and subsequently our application—as we go along, but this is a good place to start.
In this chapter, we lay the foundation for our application. We create the project’s structure, install our dependencies, set up our main and renderer processes, build our UI, and implement the Markdown-to-HTML rendering when the user enters text into the left pane.
We build the remainder of the application in phases over the next several chapters. In each chapter, you’ll download the current state of our application. This way you can flip to a chapter that covers the functionality you’re interested in without having to build the entire application from scratch.
In the first phase, our application will be able to
In later chapters, our application tracks recently opened documents using the native operating system APIs. We can drag Markdown files from the Finder or Windows Explorer onto our application and have the application immediately open that Markdown file. Our application will have its own custom application menu as well as custom context menus when we right-click on different areas of our application.
We also take advantage of OS-specific features such as updating the application’s title bar to show the file that is currently open and whether it has been changed since the last time it was saved. We also implement additional features such as updating the content in the application if some other application on the computer changes the file while we have it open.
The file structure, shown in figure 3.2, is unsurprisingly similar to the structure we agreed upon and used for our bookmark manager in the previous chapter. For the sake of simplicity and clarity as we continue to get comfortable with Electron, we keep all of the code for the main process in app/main.js and all of the code for our single renderer process in app/renderer.js. We store the app folder on a UNIX-based operating system so we can generate it quickly, as shown in the following listing. Alternatively, you can check out the master branch for this project on GitHub at https://github.com/electron-in-action/firesale.
mkdir app && touch app/index.html app/main.js app/renderer.js app/style.css
The parts of the project are
To keep things simple, we start with two dependencies in addition to Electron as our run time. We use a library called marked to handle the heavy lifting of converting Markdown to HTML.
To generate a package.json for this project, run npm init --yes. The --yes flag allows you to skip the prompts from the previous chapter. After you generate the package.json file, run the following command to install the necessary dependencies:
npm install electron marked --save
The main entry in our package.json is configured to load index.js as the main process for our application shown in figure 3.3. We need to adjust this to app/main.js. We also need to fire up a renderer process to present the user with an interface for our application. In app/main.js, let’s add the following code.
const { app, BrowserWindow } = require('electron'); let mainWindow = null; 1 app.on('ready', () => { mainWindow = new BrowserWindow(); 2 mainWindow.loadFile('index.html'); 3 mainWindow.on('closed', () => { mainWindow = null; 4 }); });
This is enough to start up our application. That said, there is not a lot going on because our main process currently loads an empty file in the renderer process.
Implementing the requisite amount of HTML and CSS to get a workable version of the wireframe in figure 3.1 is fairly easy in Electron because we need to support only one browser, and that browser supports the latest and greatest features that the web platform offers, as shown in figure 3.4.
In index.html, we add the markup in listing 3.3 to create the browser window in figure 3.5.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Fire Sale</title> <link rel="stylesheet" href="style.css" type="text/css"> </head> <body> <section class="controls"> 1 <button id="new-file">New File</button> <button id="open-file">Open File</button> <button id="save-markdown" disabled>Save File</button> <button id="revert" disabled>Revert</button> <button id="save-html">Save HTML</button> <button id="show-file" disabled>Show File</button> <button id="open-in-default" disabled>Open in Default Application</button> </section> <section class="content"> 2 <label for="markdown" hidden>Markdown Content</label> 3 <textarea class="raw-markdown" id="markdown"></textarea> <div class="rendered-html" id="html"></div> </section> </body> <script> 4 require('./renderer'); </script> </html>
Our application isn’t much to look at just yet. If you’re anything like me, you’ve been a bit skeptical about that two-column interface I introduced in the wireframe. The word easy is rarely used when discussing how to implement columns using HTML and CSS. Luckily, we can confidently use a new layout mode added to CSS3 called Flexbox to quickly define the two-column layout of our application. Flexbox makes it easy to create page layouts that behave predictably across a wide range of screen sizes, as shown in listing 3.4. It’s relatively new to CSS and—until recently—was not supported by Internet Explorer. As we discussed in chapters 1 and 2, our applications are always coupled with a recent version of Chrome, so we can confidently use the Flexbox layout mode without having to worry about cross-browser compatibility.
html { box-sizing: border-box; 1 } *, *:before, *:after { box-sizing: inherit; 2 } html, body { height: 100%; width: 100%; overflow: hidden; } body { margin: 0; padding: 0; position: absolute; } body, input { 3 font: menu; } textarea, input, div, button { outline: none; 4 margin: 0; } .controls { background-color: rgb(217, 241, 238); padding: 10px 10px 10px 10px; } button { font-size: 14px; background-color: rgb(181, 220, 216); border: none; padding: 0.5em 1em; } button:hover { background-color: rgb(156, 198, 192); } button:active { background-color: rgb(144, 182, 177); } button:disabled { background-color: rgb(196, 204, 202); } .container { display: flex; flex-direction: column; min-height: 100vh; min-width: 100vw; position: relative; } .content { height: 100vh; display: flex; 5 } .raw-markdown, .rendered-html { min-height: 100%; max-width: 50%; flex-grow: 1; 6 padding: 1em; overflow: scroll; font-size: 16px; } .raw-markdown { border: 5px solid rgb(238, 252, 250);; background-color: rgb(238, 252, 250); font-family: monospace; }
We have two major goals for the stylesheet. First, we want to take advantage of modern CSS features like Flexbox to lay out our UI. Second, we want to take small steps toward making our application look and feel a bit more like a real web application (see figure 3.6).
The box-sizing property handles an historical oddity in CSS where adding 50 pixels of padding to an element with a width of 200 pixels would cause it to be 300 pixels wide (adding 50 pixels of padding on each side), with the same being true for borders as well. When box-sizing is set to border-box, our elements respect the height and width that we set them to. Generally speaking, this is a good thing. In this CSS rule, we also have every other element and pseudoelement respect the hard work we did by setting box-sizing to border-box.
We want our applications to fit in with their native colleagues. One important step in that direction is to use the system font that all of the other applications use. That’s easier said than done. For example, despite the fact that macOS uses San Francisco as the default font throughout the operating system, it’s not available as a regular font. We set the font property to menu, which defers to the operating system to use its default font—even if we wouldn’t otherwise have access to it.
The browser puts a border around whatever UI element is currently active. In macOS, this border is a blue glow. You’ve probably never thought much about it, because we’re used to it on the web, but it looks out of place when we’re developing a desktop application. It looks especially bad in our application where one-half of the UI is effectively a large text input. By setting outline to none, we remove the unnatural glow around the active element.
In the .content, .raw-markdown, and .rendered-html rules, we implement a simple Flexbox layout, which will make our application look more like the wireframe we introduced in figure 3.1. The element with the content class will hold our two columns. We set the display property to flex to use the Flexbox technology we discussed earlier. In the next step, we set flex-grow, which specifies the grow factor for a flex item, of course. It’s probably helpful to think of this as the element’s scale in relation to its sibling. In this case, we set both columns to an equal ratio using Flexbox.
If you look closely as your application launches, you’ll notice a brief moment when the window is completely blank before Electron loads index.html and renders the DOM in the window. Users are not used to seeing this in native applications, and we can avoid it by rethinking how we launch the window.
The flash of nothingness when the application first launches makes sense if you consider the code in the main process: it creates a window and then loads content in it. What if we hide the window until the content is loaded? Then, when the UI is ready, we show the window and avoid briefly exposing an empty window.
app.on('ready', () => { mainWindow = new BrowserWindow({ show: false }); 1 mainWindow.loadFile('index.html'); mainWindow.once('ready-to-show', () => { 2 mainWindow.show(); 3 }); mainWindow.on('closed', () => { mainWindow = null; }); });
We passed an object to the BrowserWindow constructor, setting it as hidden by default. When the BrowserWindow instance fires its ’ready-to-show’ event, we’ll call its show() method, which will bring it out of hiding after the UI is fully ready to go. This approach is even more useful when the application is loading a remote resource over the network, which is likely to take much longer to initialize the page.
Let’s put a stake in the ground by getting some of the basic functionality in place. For starters, we want to update the rendered HTML view in the right pane whenever the Markdown in the left pane changes (see figure 3.7). This is where our one dependency—marked—comes in to play.
Bringing in our dependencies is easy because we can use Node’s require to pull in marked. Let’s add the following in app/renderer.js.
const marked = require('marked');
Now, we have access to Marked using marked. Given our discussion of the application’s functionality along with the diagram in figure 3.7, you’ve probably begun to suspect that we’ll be working with the #markdown text area and the #html element a fair amount as we develop our application. Let’s use a pair of variables to store a reference to each element so that they’re easier to work with, as shown in listing 3.7. While we’re at it, let’s also create variables for each of the buttons along the top of the UI.
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');
We also render Markdown into htmlView fairly frequently, so we want to give ourselves a function to make this easier for us in the future.
const renderMarkdownToHtml = (markdown) => { htmlView.innerHTML = marked(markdown, { sanitize: true }); };
marked takes the Markdown content we want to render as the first argument and an object of options as the second argument. We’d like to protect ourselves from accidental script injections, so we pass in an object with the sanitize property set to true.
Finally, we add an event listener to markdownView that on keyup will read its contents (which, in textarea elements, is stored in its value property), run them through marked, and then load them into htmlView. The result is shown in figure 3.8.
markdownView.addEventListener('keyup', (event) => { const currentContent = event.target.value; renderMarkdownToHtml(currentContent); });
The basic functionality is in place and we’re ready to begin working on the features that would only be possible in an Electron application—starting with reading and writing files from and to the filesystem. When all is said and done, the renderer process of our application should look like this.
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); });
In an ideal world, we’d never make mistakes when writing code. APIs and methods would never change between versions and your author wouldn’t have to hold his breath every time a new version of a dependency used by the applications in this book was released. We don’t live in that world. Thus, we have developer tools at our disposal to aide us in tracking down and—hopefully—eliminating bugs.
Everything has been going pretty smoothly so far, but it probably won’t be long before we’re going to have to debug some tricky situation. Because Electron applications are based on Chrome, it’s no surprise that we have access to the Chrome Developer Tools when building Electron applications (figure 3.9).
Debugging the renderer process is relatively straightforward. Electron’s default application menu provides a command for opening up the Chrome Developer Tools in our application. In chapter 6, we’ll learn how to create our own custom menu and eliminate this feature in the event that you’d prefer not to expose it your users.
There are also two other ways to access the Developer Tools. At any point, you can press Command-Option-I on macOS or Control-Shift-I on Windows or Linux to open up the tools (figure 3.10). In addition, you can trigger the Developer Tools programmatically. The webContents property on BrowserWindow instances has a method called openDevTools(). This method, as explained in listing 3.11, will open the Developer Tools in the BrowserWindow it’s called on.
app.on('ready', () => { mainWindow = new BrowserWindow(); mainWindow.loadFile('index.html'); mainWindow.once('ready-to-show', () => { mainWindow.show(); mainWindow.webContents.openDevTools(); 1 }); mainWindow.on('closed', () => { mainWindow = null; }); });
Debugging the main process is not so easy. Node Inspector, a common tool for debugging Node.js applications, is not fully supported by Electron at this time. You can start your Electron application in debug mode using the --debug flag, which will—by default—enable remote debugging on port 5858.
Limited support for using Node Inspector with Electron is available in the official documentation. As this is still in a bit of flux for the time being, you should review the most recent version of the documentation if you are not using Visual Studio Code (http://electron.atom.io/docs/tutorial/debugging-main-process/). That said, I haven’t found this technique particularly stable and wouldn’t recommend it. Your mileage may vary.
Visual Studio Code is a free, open source IDE available for Windows, Linux, and macOS and is—coincidentally—built on top of Electron by Microsoft. Visual Studio Code comes with a rich set of tools for debugging Node applications that make it much easier to debug Electron applications than noted previously. A quick way to set up a build task is to ask Visual Studio Code to build the application without a build task. Press Control-Shift-B on Windows or Command-Shift-B on macOS and you’ll be prompted to create a build task, as shown in figure 3.11.
Clicking on the Configure Build Task menu item will prompt you to select whether you want to create a “start” or “test” task. Choosing “start” will generate a task that calls npm start. Choosing “test” will generate npm test. Listing 3.12 is an example of what a “start” task looks like.
{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "type": "npm", "script": "start", "group": { "kind": "build", "isDefault": true } } ] }
Now, when you press Control-Shift-B on Windows or Command-Shift-B on macOS, your Electron application will start up. Not only is this important in order to set up debugging within Visual Studio Code, it’s also a convenient way to start up your application in general. The next step is to set up Visual Studio Code to launch the application and connect it to its built-in debugger (figure 3.12).
To create a launch task, go the Debug tab in the left pane and click on the small gear in the upper-left corner. Visual Studio Code will ask you what kind of configuration file you’d like to create. Select Node and replace the contents of the file with listing 3.13.
{ "version": "0.2.0", "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "cwd": "${workspaceRoot}", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" }, "args": "." ] } ] }
With this configuration file in place, you can click on the left margin of any line in your main process to set a breakpoint and then press F5 to run the application. Execution will pause at the breakpoint, allowing you to inspect the call stack, determine what variables are in scope, and interact with a live console. Breakpoints aren’t the only way to debug your code. You can also watch for particular expressions or drop into the debugger whenever an uncaught exception is thrown (figure 3.13).
There is a high chance that you’re not using Visual Studio Code. That’s fine. It’s not a prerequisite for this book and you will almost definitely be fine using the text editor or IDE you’re most comfortable with. In addition, Visual Studio Code isn’t the only one with support for debugging the main process. For example, you can find details for configuring WebStorm here: http://mng.bz/Y5T6.
18.225.31.159