Create a Desktop App with Angular 2 and Electron

Last week, I took part in the Google Developer Day held in Beijing. The Angular team introduced their new Angular 2. Angular is a development platform for building mobile and desktop web applications.

Google-Developer-Day-2016-Beijing

This tutorial will  show how to configure and use Angular 2 web components with Electron framework for creating native cross-platform applications with web technologies.

As recommended by Angular team, TypeScript will be used throughout this tutorial. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.

You will get a link to finished working example at GitHub at the end of the article.

Prerequisites

Before start, please make sure that node, npm, typescript, typings are installed.

npm install -g typescript typings

Setup Electron with TypeScript

The first thing you want to do is to initialize your project using package.json:

{
    "name": "App-Name",
    "version": "1.0.0",
    "main": "main.ts",
    "scripts": {
        "start": "tsc main.ts && electron main.js",
        "clean": "rm -Rf *.log *.js",
        "tsc:w": "tsc -w",
        "typings": "typings",
        "postinstall": "typings install"
    },
    "author": "Your Name",
    "license": "GPL-2.0",
    "devDependencies": {
        "typescript": "~2.1.4",
        "typings": "^2.0.0"
    },
    "dependencies": {
        "electron": "^1.4.12"
    }
}

Typings

The type definition files used by this project are managed by the "typings" TypeScript Definition Manager, version 1.0 or higher. It is not necessary to have "typings" installed just to run this application. If you haven't installed this, you'll need to install "typings" as a global NPM module:

npm install -g typings

The type definitions are committed to source control, as the `typings.json` file and the `typings` subdirectory. To get the latest type definitions, delete that file and subdirectory and replace them by running following commands:

typings install dt~electron/github-electron --save --global
typings install dt~node --save --global

After that, a directory named `typings` and a file named `typings.json` are created.

TypeScript

Next, you’ll need to setup TypeScript. For this, you’ll need a tsconfig.json file in your root. I’ll just give you the file you’ll need here:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false
    },
    "exclude": [
        "node_modules",
        "typings"
    ]
}

Please note the exclude option. This setting greatly improves performance when using Atom or IntelliJ for development.

The Electron Shell with TypeScript

Now comes the fun part. The normal code for creating the shell for an Electron application in JavaScript normally looks something like this:

const electron = require('electron');
// Module to control application life.
const app = electron.app;  

// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;  

// Keep a global reference of the window object, 
// if you don't, the window will be closed automatically
// when the JavaScript object is garbage collected.
var mainWindow = null;
// Quit when all windows are closed.
app.on('window-all-closed', function() {
    // On OS X it is common for applications and their
    // menu barto stay active until the user quits 
    // explicitly with Cmd + Q
    if (process.platform != 'darwin') {
        app.quit();
    }
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
    // Create the browser window.
    mainWindow = new BrowserWindow({width: 800, height: 600});

    // and load the index.html of the app.
    mainWindow.loadURL('file://' + __dirname + '/index.html');

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

    // Emitted when the window is closed.
    mainWindow.on('closed', function() {
        // 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;
    });
});

The trick we want to perform is to create a TypeScript class that does essentially the same thing but using TypeScript syntax.

/// <reference path="typings/index.d.ts" />
import electron = require("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.
function createWindow() {
    // Create the browser window.
    mainWindow = new BrowserWindow({width: 800, height: 600});

    // and load the index.html of the app.
    mainWindow.loadURL(`file://${__dirname}/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.
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();
    }
});

Finally, created a HTML file named `index.html` containing following contents:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
</head>
<body>
    <h1>Hello World!</h1>
    <!-- All of the Node.js APIs are available in this renderer process. -->
    We are using node <script>document.write(process.versions.node)</script>,
    Chromium <script>document.write(process.versions.chrome)</script>,
    and Electron <script>document.write(process.versions.electron)</script>.
</body>
</html>

Running application

npm install && npm start

and you should see something like the following:

Electron-Quick-Start

Setup Electron with Angular 2

First of all, you need to add dependencies of Angular to package.json:

{
    "name": "App-Name",
    "version": "1.0.0",
    "main": "main.ts",
    "scripts": {
        "start": "tsc && concurrently \"npm run tsc:w\" \"electron main.js\"",
        "clean": "rm -Rf *.log *.js *.map app/*.js app/*.map",
        "tsc:w": "tsc -w",
        "typings": "typings",
        "postinstall": "typings install"
    },
    "author": "Your Name",
    "license": "GPL-2.0",
    "devDependencies": {
        "concurrently": "^3.0.0",
        "electron-packager": "^8.4.0",
        "typescript": "~2.1.4",
        "typings": "^2.0.0"
    },
    "dependencies": {
        "@angular/common": "^2.3.0",
        "@angular/compiler": "^2.3.0",
        "@angular/core": "^2.3.0",
        "@angular/http": "^2.3.0",
        "@angular/material": "2.0.0-alpha.11-3",
        "@angular/platform-browser": "^2.3.0",
        "@angular/platform-browser-dynamic": "^2.3.0",
        "electron": "^1.4.12",
        "reflect-metadata": "^0.1.8",
        "rxjs": "5.0.0-rc.4",
        "systemjs": "^0.19.41",
        "zone.js": "^0.7.2"
    }
}

You will need installing new libraries once updating your package.json file:

npm install

Then, you need to install another two typings for Angular:

typings install dt~core-js --save --global
typings install dt~jasmine --save --global

Next, create file named system.config.ts:

/** Type declaration for ambient System. */
declare var System: any;

/**
 * System configuration for Angular samples
 * Adjust as necessary for your application needs.
 */
System.config({
    paths: {
        // paths serve as alias
        'npm:': 'node_modules/'
    },
    // map tells the System loader where to look for things
    map: {
        // our app is within the app folder
        app: 'app',
        // angular bundles
        '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
        '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
        '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
        '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
        '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
        '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
        // other libraries
        'rxjs': 'npm:rxjs',
    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
        app: {
            main: './main.js',
            defaultExtension: 'js'
        },
        '@angular/material': {
            format: 'cjs',
            main: 'npm:@angular/material/material.umd.js'
        },
        rxjs: {
            defaultExtension: 'js'
        }
    }
});

Modify index.html and include javascripts for Angular:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Angular 2 with Electron</title>
    <!-- 1. Load libraries -->
    <!-- Polyfill(s) for older browsers -->
    <script type="text/javascript" src="node_modules/core-js/client/shim.min.js"></script>
    <script type="text/javascript" src="node_modules/zone.js/dist/zone.js"></script>
    <script type="text/javascript" src="node_modules/reflect-metadata/Reflect.js"></script>
    <script type="text/javascript" src="node_modules/systemjs/dist/system.src.js"></script>
    <!-- 2. Configure SystemJS -->
    <script type="text/javascript" src="systemjs.config.js"></script>
    <script type="text/javascript">
        System.import('app').catch(function(err){ console.error(err); });
    </script>
</head>
<!-- 3. Display the application -->
<body>
    <my-app>Loading...</my-app>
    <!-- All of the Node.js APIs are available in this renderer process. -->
    We are using node <script>document.write(process.versions.node)</script>,
    Chromium <script>document.write(process.versions.chrome)</script>,
    and Electron <script>document.write(process.versions.electron)</script>.
</body>
</html>

Simple Angular 2 application

This step does not differ from the Step 2: Our first Angular component chapter of the 5 min quickstart. Please check original article for more details on implementation.

Create an app subfolder at project root directory to hold the following files:

app/app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: '<h1>My First Angular 2 App with Electron</h1>'
})
export class AppComponent { }
app/main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule }              from './app.module';

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Running application

npm start

The start command compiles your TypeScript code and launches Electron instance with your application getting loaded automatically.

Electron-Angular-2

Also note that start command runs TypeScript compiler in watch mode. Every time you change your code it should be enough just reloading Electron via "View ⟶ Reload" or with Cmd-R (Ctrl-R on Windows).

FAQ

  • Question: How to deal with the error message: "Duplicate identifier 'PropertyKey'"?
    Answer: 
    Try following solutions:

    1. remove "@types/core-js": "0.9.34", from devDependencies in package.json.
    2. Add following lines to tsconfig.json:
      "exclude": [
          "node_modules",
          "typings"
      ]
      

Source Code

The source code for this tutorial is open sourced on GitHub.

Reference

  • https://angular.io/
  • https://www.typescriptlang.org/
  • http://denisvuyka.github.io/2016/07/08/angular2-electron.html
  • http://blog.dmbcllc.com/typescript-and-electron-the-right-way/
  • Frank Paepens

    The command: typings install dt~electron/github-electron --save --global
    is not working. (Any idea? Thx!)

    This was the error in the console:

    typings ERR! message Unable to find "electron/github-electron" ("dt") in the registry.
    typings ERR! message We could use your help adding these typings to the registry: https://github.com/typings/registry
    typings ERR! caused by https://api.typings.org/entries/dt/electron%2Fgithub-electron/versions/latest responded with 404, expected it to equal 200
    typings ERR!
    typings ERR! cwd C:Usersfrank.DVITAppDataRoamingnpmnode_modules
    typings ERR! system Windows_NT 10.0.10586
    typings ERR! command "C:\Program Files\nodejs\node.exe" "C:\Users\frank.DVIT\AppData\Roaming\npm\node_modules\typings\dist\bin.js" "install" "dt~electron/github-electron" "--save" "--global"
    typings ERR! node -v v6.9.1
    typings ERR! typings -v 2.1.0
    typings ERR!
    typings ERR! If you need help, you may report this error at:
    typings ERR!

    • Please try this:
      typings install dt~electron/github-electron --save --global

      • Frank Paepens

        Thx for the reply!
        But that is exactly what I tried and what was causing the error...

        • I'm sorry. Maybe you can use this:
          typings install dt~electron --save --global

  • Pavol Maški Maškrta

    ty for great work and help... i have one question i have this error in console:
    file:///D:/app2/node_modules/core-js/client/shim.min.js Failed to load resource: net::ERR_FILE_NOT_FOUND

    any idea why?

    • Can you find this file in D:/app2/node_modules/core-js/client/shim.min.js?
      I couldn't reproduce your error.

  • Wang Haoran

    校友你好

  • Praveen Kumar S

    how to use electron with angular-cli strucure?. I've already done a project with electron + angular 1.5 which is so fine. But according to angular-cli how to handle electron inside of it's components

Contact Us
  • Room 311, Zonghe Building, Harbin Institute of Technology
  • cshzxie [at] gmail.com