How to build a desktop app using Electron

Amit Kothari

October 17th, 2016

Desktop apps are making a comeback. Even companies with cloud-based applications with awesome web apps are investing in desktop apps to offer a better user experience. One example is team collaboration tool called Slack. They built a really good desktop app with web technologies using Electron.

Electron is an open source framework used to build cross-platform desktop apps using web technologies. It uses Node.js and Chromium and allows us to develop desktop GUI apps using HTML, CSS and JavaScript. Electron is developed by GitHub, initially for Atom editor but now used by many companies, including Slack, Wordpress, Microsoft and Docker to name a few.

Electron apps are web apps running in embedded Chromium web browser, with access to the full suite of Node.js modules and underlying operating system.

In this post we will build a simple desktop app using Electron.

Hello Electron

Let’s start by creating a simple app. Before we start, we need Node.js and npm installed. Follow the instructions on the Node.js website if you do not have these installed already.

Create a new director for your application and inside the app directory, create a package.json file by using the npm init command. Follow the prompts and remember to set main.js as the entry point.

Once the file is generated, install electron-prebuild, which is the precomplied version of electron, and add it as a dev depenency in the package.json using the command npm install --save-dev electron-prebuilt. Also add "start": "electron ." under scripts, which we will use later to start our app.

The package.json file will look something like this:

{
  "name": "electron-tutorial",
  "version": "1.0.0",
  "description": "Electron Tutorial ",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron-prebuilt": "^1.3.3"
  }
}

Create a file main.js with the following content:

const {app, BrowserWindow} = require('electron');

// Global reference of the window object.
let mainWindow;

// When Electron finish initialization, create window and load app index.html
app.on('ready', () => {
    mainWindow = new BrowserWindow({ width: 800, height: 600 });
    mainWindow.loadURL(`file://${__dirname}/index.html`);
});

We defined main.js as the entry point to our app in package.json. In main.js the electron app module controls the application lifecyle and BrowserWindow is used to create a native browser window. When Electron finishes initializing and our app is ready, we create a browser window to load our web page—index.html.

As mentioned in the Electron documentation, remember to keep a global reference of the window object to avoid it from closing automatically when the JavaScript garbage collector kicks in.

Finally, create the index.html file:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Hello Electron</title>
</head>

<body>
    <h1>Hello Electron</h1>
</body>

</html>

We can now start our app by running the npm start command.

Testing the Electron app

Let’s write some integration tests for our app using Spectron.

spectron allows us to test Electron apps using ChromeDriver and WebdriverIO. It is a test framework that is agnostic, but for this example, we will use mocha to write the tests.

Let’s start by adding spectron and mocha as dev dependecies using the npm install --save-dev spectron and npm install --save-dev mocha commands.

Then add "test": "./node_modules/mocha/bin/mocha" under scripts in the package.json file. This will be used to run our tests later.

The package.json should look something like this:

{
  "name": "electron-tutorial",
  "version": "1.0.0",
  "description": "Electron Tutorial ",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "test": "./node_modules/mocha/bin/mocha"
  },
  "devDependencies": {
    "electron-prebuilt": "^1.3.3",
    "mocha": "^3.0.2",
    "spectron": "^3.3.0"
  }
}

Now that we have all the dependencies installed, let’s write some tests. Create a directory called test and a file called test.js inside it. Copy the following content to test.js:

var Application = require('spectron').Application;
var electron = require('electron-prebuilt');
var assert = require('assert');

describe('Sample app', function () {
    var app;

    beforeEach(function () {
        app = new Application({ path: electron, args: ['.'] });
        return app.start();
    });

    afterEach(function () {
        if (app && app.isRunning()) {
            return app.stop();
        }
    });

    it('should show initial window', function () {
        return app.browserWindow.isVisible()
            .then(function (isVisible) {
                assert.equal(isVisible, true);
            });
    });

    it('should have correct app title', function () {
        return app.client.getTitle()
            .then(function (title) {
                assert.equal(title, 'Hello Electron')
            });
    });
});

Here we have couple of simple tests. We start the app before each test and stop after each test. The first test is to verify that the app's browserWindow is visible, and the second test is to verify the app’s title.

We can run these tests using the npm run test command.

spectron not only allows us to easily set up and tear down our app, but also give access to various APIs, allowing us to write sophisticated tests covering various business requirements. Please have a look at their documentation for more details.

Packaging our app

Now that we have a basic app, we are ready to package and build it for distribution. We will use electron-builder for this, which offers a complete solution to distribute apps on different platforms with the option to auto-update.

It is recommended to use two separate package.jsons when using electron-builder, one for the development environment and build scripts and another one with app dependencies. But for our simple app, we can just use one package.json file.

Let’s start by adding electron-builder as dev dependency using command npm install --save-dev electron-builder.

Make sure you have the name, desciption, version and author defined in package.json.

You also need to add electron-builder-specific options as build property in package.json:

"build": {
  "appId": "com.amitkothari.electronsample",
  "category": "public.app-category.productivity"
}

For Mac OS, we need to specify appId and category. Look at the documentation for options for other platforms.

Finally add script in package.json to package and build the app:

"dist": "build"

The updated package.json will look like this:

{
  "name": "electron-tutorial",
  "version": "1.0.0",
  "description": "Electron Tutorial ",
  "author": "Amit Kothari",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "test": "./node_modules/mocha/bin/mocha",
    "dist": "build"
  },
  "devDependencies": {
    "electron-prebuilt": "^1.3.3",
    "mocha": "^3.0.2",
    "spectron": "^3.3.0",
    "electron-builder": "^5.25.1"
  },
  "build": {
    "appId": "com.amitkothari.electronsample",
    "category": "public.app-category.productivity"
  }
}

Next we need to create a build directory under our project root directory. In this, put a file background.png for the Mac OS DMG background and icon.icns for app icon.

We can now package our app by running the npm run dist command.

Todo App

We’ve built a very simple app, but Electron apps can do more than just show static text. Lets add some dynamic behavior to our app and convert it into a Todo list manager.

We can use any JavaScript framework of choice, from AngularJS to React, with Electron, but for this example, we will use plain JavaScript.

To start with, let’s update our index.html to display a todo list:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Hello Electron</title>
    <link rel="stylesheet" type="text/css" href="./style.css">
</head>

<body>
    <div class="container">
        <ul id="todoList"></ul>
        <textarea id="todoInput" placeholder="What needs to be done ?"></textarea>
        <button id="addTodoButton">Add to list</button>
    </div>
</body>

<script>require('./app.js')</script>

</html>

We also included style.css and app.js in index.html. All our CSS will be in style.css and our app logic will be in app.js.

Create the style.css file with the following content:

body {
    margin: 0;
}

ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
}

li {
    padding: 10px;
    border-bottom: 1px solid #ddd;
}

button {
    background-color: black;
    color: #fff;
    margin: 5px;
    padding: 5px;
    cursor: pointer;
    border: none;
    font-size: 12px;
}

.container {
    width: 100%;
}

#todoInput {
    float: left;
    display: block;
    overflow: auto;
    margin: 15px;
    padding: 10px;
    font-size: 12px;
    width: 250px;
}

#addTodoButton {
    float: left;
    margin: 25px 10px;
}

And finally create the app.js file:

(function () {

    const addTodoButton = document.getElementById('addTodoButton');
    const todoList = document.getElementById('todoList');

    // Create delete button for todo item
    const createTodoDeleteButton = () => {
        const deleteButton = document.createElement("button");
        deleteButton.innerHTML = "X";
        deleteButton.onclick = function () {
            this.parentNode.outerHTML = "";
        };
        return deleteButton;
    }

    // Create element to show todo text
    const createTodoText = (todo) => {
        const todoText = document.createElement("span");
        todoText.innerHTML = todo;
        return todoText;
    }

    // Create a todo item with delete button and text
    const createTodoItem = (todo) => {
        const todoItem = document.createElement("li");
        todoItem.appendChild(createTodoDeleteButton());
        todoItem.appendChild(createTodoText(todo));
        return todoItem;
    }

    // Clear input field
    const clearTodoInputField = () => {
        document.getElementById("todoInput").value = "";
    }

    // Add new todo item and clear input field
    const addTodoItem = () => {
        const todo = document.getElementById('todoInput').value;
        if (todo) {
            todoList.appendChild(createTodoItem(todo));
            clearTodoInputField();
        }
    }

    addTodoButton.addEventListener("click", addTodoItem, false);
} ());

Our app.js has a self invoking function which registers a listener (addTodoItem) on addTodoButton click event. On add button click event, the addTodoItem function will add a new todo item and clear the text area.

Run the app again using the npm start command.

Conclusion

We built a very simple app, but it shows the potential of Electron. As stated on the Electron website, if you can build a website, you can build a desktop app. I hope you find this post interesting. If you have built an application with Electron, please share it with us.

About the author

Amit Kothari is a full-stack software developer based in Melbourne, Australia. He has 10+ years experience in designing and implementing software, mainly in Java/JEE. His recent experience is in building web applications using JavaScript frameworks such as React and AngularJS and backend microservices/REST API in Java.
He is passionate about lean software development and continuous delivery.