Electron+Angular2: Getting Started

Electron+Angular2: A cross platform IRC client

Motivation

I’ve been looking to work my way through a full electron application, as well as learning the ins and outs of Angular 2. So why not do both! As for why make an IRC client? I have found that there are no good cross platform clients for IRC, and I am a dirty Twitch viewer who hates having the chat log take up precious screen real estate. So now you know.

Getting Started

First things first, I decided to make my life easier using the angular-cli tool. Install it using that oh so simple one liner npm install -g angular-cli (well I used yarn as in yarn global add angular-cli). Then make your new angular app using ng new [app-name]. Finally, pop into the directory that was created (cd [app-name]) and explore your newly created angular goodness.

The structure of the application

The first place you want to look is at the src folder – that’s where the actual application lives and breathes (well metaphorically speaking). You should have a single component in that folder all contained under the app directory. That component will be your root component that handles pulling the app together. You also have an assets folder for any static assets you may have, as well as an environments folder that defines different environment variables (prod vs dev vs whatever). At the top level of the src folder you will see your base index.html file that gets loaded by the app, the main.ts (more on typescript later) file that handles bootstrapping the application as well as a few other less important files (well less important for now).

Restructuring things for Electron

As it stands this app will run fine in a browser, but we want to be cool and run this in Electron! So first things first, rename that pesky src directory to renderer-src. This directory is going to contain all of the code one taht is rendered inside the app frame. We’ll be working here primarily for the fancy front end stuff we’ll be doing. Since we changed that we need to open up angular-cli.json and make some changes. First things first locate the first (and from what I can tell only viable) element of the apps array. You will see a root key. Change that key from src to renderer-src. While we’re here go ahead and change the outDir from dist to dist/renderere so we can use that dist folder for both the renderer code and main code.

Next let’s get set up for the main process. Go ahead and create the main-src directory in the root of your project and create a nice new main.ts file in there. That main.ts file is going to be the actual entry point for electron and will do all of the bootstrapping. The basic default main.ts file looks something like this:

import * as electron from 'electron';
let app = electron.app;
let BrowserWindow = electron.BrowserWindow;

// Global reference to the main window, so the garbage collector doesn't close it.
let mainWindow : Electron.BrowserWindow;

// Opens the main window, with a native menu bar.
let createWindow = () => {
    // Create the browser window.
    mainWindow = new BrowserWindow({
        minWidth: 800,
        minHeight: 600,
        width: 1024,
        height: 768,
        frame: true
    });

    // and load the index.html of the app.
    mainWindow.loadURL(`file://${__dirname}/../renderer/index.html`);

    // Open the DevTools.
    // mainWindow.webContents.openDevTools();

    // Emitted when the window is closed.
    mainWindow.on("closed", () => {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        mainWindow = null;
    });
}

// Call 'createWindow()' on startup.
app.on("ready", () => {
    createWindow();
});

// On OS X it is common for applications and their menu bar to stay active until the user quits explicitly
// with Cmd + Q.
//TODO consider if I want to change this behaviour or not?
app.on("window-all-closed", () => {
    if (process.platform !== "darwin") {
        app.quit()
    }
});

// On OS X it's common to re-create a window in the app when the dock icon is clicked and there are no other
// windows open.
app.on("activate", () => {
    if (mainWindow === null) {
        createWindow();
    }
});

However as it stands this file won’t work for one reason – that pesky file://${__dirname}/../renderer/index.html. That is intentional, but I wanted to warn you before you got all trigger happy and tried to run it.

Build Scripts

I’m as lazy as the next developer and hate the idea of having to remember long commands and all the options I need so we’re going to make life easier and set up some npm scripts. Since I started by using the angular-cli tool ng we’ll stick to using that for building the renderer code. For the main code we turn to typings to compile our typescript to javascript. tsc can be made much easier if you go ahead and use a tsconfig.json file to manage your settings, to avoid munging ng we’re going to name our config file tsconfig-main.json and call it with the -p option. The tsconfig-main.json file I end up with looks like this:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": false,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false,
        "outDir": "dist/main"
    },
    "include": [
        "main-src/**/*"
    ]
}

This file will make tsc compile everything in the main-src directory and output it to the dist/main directory for us to use. Most of the other options should be self-explanatory, if not take a glance at the tsconfig.json documentation.

When developing we don’t want to have to re-run a command every time we make a change (yes I’m looking at you, don’t do this, it’s annoying and a waste of time), so let’s set up our npm scripts to build, run and watch for us. Here is the scripts block of my package.json

"scripts": {
    "ng": "ng",
    "clean": "rm -rf dist/",
    "ng:watch": "ng build -w",
    "ng:build": "ng build",
    "tsc:watch": "tsc -w -p tsconfig-main.json",
    "tsc:build": "tsc -p tsconfig-main.json",
    "electron:start": "electron dist/main/main.js",
    "start": "npm-run-all clean ng:build tsc:build --parallel electron:start ng:watch tsc:watch",
    "lint": "tslint \"renderer-src/**/*.ts\" \"main-src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update --standalone false --gecko false",
    "e2e": "protractor"
}

So let me run through these in order. The first just makes it easier to call the angular-cli command ng without having to prefix each call with node_modules/.bin (though is npm run ng that much easier?). The next, clean, removes the built files to prevent any messiness, probably unnecessary, but not harmful certainly. Now things get interesting, we have ng:build and ng:watch that call ng and build the angular part of the app (the -w option means ng keeps running and will rebuild if a file changes, useful for development). Next we have our tsc parts that build the main process code. Again, the -w option leaves the compiler running to rebuild on changes. electron:start is pretty simple it launches our electron app by loading the compiled entry file. Now the big boy – start – this does a whole lot in one line so bear with me. npm-run-all (https://www.npmjs.com/package/npm-run-all) is a useful little tool that allows for composition of tasks in serial or parallel. The first group will run in serial as we didn’t prefix it with the --parallel option, it will clean any old compiled code, then build first the renderer process code, then the main process code using the scripts I talked about earlier. Next it runs three other commands in parallel – it starts the electron app itself, and then starts the watchers for both the renderer and main process code so any changes we make are automatically built, and we just have to reload the electron window.

Running the App

So I’ve talked about some core concepts, the structure, and the build scripts needed to make all this work, so let’s finally get to it. Pop open a terminal window and cd into your project directory. Now run npm start and watch as the terminal starts blowing up with information. Once all of the compilation is done the app should launch and boy howdy…nothing happens. There is one gotcha with mixing Anular with Electron – open up the file renderer-src/index.html and look for the base tag. Notice how its value is / well that is going to screw up all sorts of things when working with a filesystem, not URLs. Change that value to ./, save index.html and reload the Electron app (cmd+r or ctrl+r depending on your platform). Now look at that – we’ve got ourself an Angular app running inside Electron. Let me make one last tweak to show you how all of the watchers work. Pop open renderer-src/app/app.component.ts and locate the title attribute inside the AppComponent class. Change that to something more fun. I went with mp-irc Works! (not very fun I know). Now save that file, and watch as the compiler updates. Once you think that’s good to go jump into your Electron app and reload again to see your changes live.

That’s all for now – but this is only part 1. Next post will start digging into the nitty gritty of getting the renderer and main processses to talk to each other, plus maybe a little work on getting the IRC connections running.

Source Code

Check out the source for this part of the article on github

Also read...

Leave a Reply

Your email address will not be published. Required fields are marked *