Chapter 3. Building a notes application

This chapter covers

  • Introducing the application we’ll build over the next few chapters
  • Configuring our CSS stylesheet to look more like a native application
  • Reviewing the relationship between the main and renderer processes in Electron
  • Implementing the basic functionality for our main and renderer processes
  • Accessing the Chrome Developer Tools in the renderer process in Electron

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.

3.1. Defining our application

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.

Figure 3.1. A wireframe of our application shows that the user can enter text in the left pane or load it from a file from the user’s filesystem.

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

  • Open and save files to the filesystem
  • Take Markdown content from those files
  • Render the Markdown content as HTML
  • Save the resulting HTML to the filesystem
  • Write the resulting HTML to the clipboard

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.

3.2. Laying the foundation

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.

Figure 3.2. The structure of our project

Listing 3.1. Generating the application’s file structure
mkdir app && touch app/index.html app/main.js app/renderer.js app/style.css

The parts of the project are

  • index.html—Contains all of the HTML markup that provides structure for our UI
  • main.js—Contains the code for our main process
  • renderer.js—Contains all of the code for interactivity of our UI
  • style.css—Contains the CSS that styles our UI
  • package.json—Contains all of our dependencies and points Electron to main.js when it loads the main process on start-up

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

3.3. Bootstrapping the application

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.

Figure 3.3. Electron starts by looking for our single main process, which is in charge of spawning one or more renderer processes in charge of displaying our UI.

Listing 3.2. Bootstrapping the main process: ./app/main.js
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
  });
});

  • 1 Declares mainWindow at the top level so that it won’t be collected as garbage after the “ready” event completes
  • 2 Creates a new BrowserWindow using the default properties
  • 3 Loads app/index.html in the BrowserWindow instance we just created
  • 4 Sets the process back to null when the window is closed

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.

3.3.1. Implementing the UI

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.

Figure 3.4. The main process will create a renderer process and tell it load index.html, which will then load CSS and JavaScript just as it would in the browser.

In index.html, we add the markup in listing 3.3 to create the browser window in figure 3.5.

Figure 3.5. The unstyled beginnings our first Electron application

Listing 3.3. Our application’s markup: ./app/index.html
<!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>

  • 1 The .controls section added the buttons for opening and saving files along the top. We’ll add functionality to these buttons later.
  • 2 Our application allows us to write and edit content in the text area with the class of .raw-markdown and render that content in the div element with the class of .rendered-html.
  • 3 The <label> tags are optional and included to make the application more accessible for visually impaired users.
  • 4 In the <script> tags at the end of the file, we require the code for our renderer process, which lives in renderer.js in the same directory.

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.

Listing 3.4. Using Flexbox to create page layouts: ./app/style.css
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;
}

  • 1 Opts in to an updated CSS box model that will correctly set the width and height of elements
  • 2 Passes this setting to every other element and pseudoelement on the page
  • 3 Uses the operating system’s default font throughout the application
  • 4 Removes the browser’s default highlighting around active input fields
  • 5 Uses Flexbox to align the two panes of our application
  • 6 Sets both panes to an equal width using Flexbox

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).

Figure 3.6. Our application has been given some basic styling using modern features of CSS.

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.

3.3.2. Gracefully displaying the browser window

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.

Listing 3.5. Gracefully showing the window when the DOM’s ready: ./app/main.js
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;
  });
});

  • 1 Begin by hiding the window when it’s first created.
  • 2 Add a single event listener to the window’s “ready-to-show” event.
  • 3 Show the window when the DOM is ready.

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.

3.4. Implementing the base functionality

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.

Listing 3.6. Requiring our dependencies: ./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.

Figure 3.7. We’ll add an event listener to the left pane that will render the Markdown as HTML and display it in the right pane.

Listing 3.7. Caching DOM selectors: ./app/renderer.js
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.

Listing 3.8. Converting Markdown to HTML: ./app/renderer.js
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.

Figure 3.8. Our application takes the content typed by the user in the left pane and automatically renders it as HTML in the right pane. This content was provided by the user and is not part of our application.

Listing 3.9. Re-rendering the HTML when Markdown changes: ./app/renderer.js
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.

Listing 3.10. 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);
});

3.5. Debugging an Electron application

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.

3.5.1. Debugging renderer processes

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).

Figure 3.9. The Chrome Developer Tools are available in the renderer process just as they would be in a browser-based application.

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.

Figure 3.10. The tools can be toggled on and off in the default menu provided by Electron. You can also toggle them using Control-Shift-I on Windows or Command-Option-I on macOS.

Listing 3.11. Opening the Developer Tools from the main process: ./app/main.js
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;
  });
});

  • 1 We can programmatically trigger the opening of the Developer Tools on the main window as soon as it’s loaded.

3.5.2. Debugging the main process

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.

3.5.3. Debugging the main process with Visual Studio Code

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.

Figure 3.11. Triggering the build task without one in place will prompt Visual Studio Code to create one on your behalf.

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.

Listing 3.12. Setting up a build task in Visual Studio Code for Windows: tasks.json
{
  // 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).

Figure 3.12. Inside the Debug tab, click on the gear and Visual Studio Code will create a configuration file for launching the debugger on your behalf.

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.

Listing 3.13. Setting up a launch task for Visual Studio Code for Windows: launch.json
{
  "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).

Figure 3.13. The debugger built in to Visual Studio Code allows you to pause the execution of your application and drop in to investigate bugs.

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.

Summary

  • Over the next few chapters, we’ll be working on a Markdown-to-HTML renderer.
  • Flexbox is supported by modern browsers and allows us to easily implement a two-pane interface that will adapt as the user changes the size of the window.
  • Chrome Developer Tools are available in all renderer processes and can be triggered from the default application in Electron, a keyboard shortcut, or from the main process.
  • The Node Inspector is not fully supported in Electron at this time.
  • Visual Studio Code provides a rich set of tools for debugging problems in the main process of your application.
..................Content has been hidden....................

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