Electron with Angular Tutorial

Advertisement

Advertisement

Introduction

Electron is an amazing framework that lets you create desktop application using JavaScript, HTML, and CSS. It is essentially a web application that is self contained as a desktop application. The Electron API lets you access native system elements like the system tray icons, menus, dialogs, etc.

In this guide, we will look at how to create an Electron application with the Angular framework using TypeScript. We will cover:

  • Building a project from scratch
  • Packaging the desktop application for distribution
  • Using live reloading for development
  • Using Electron APIs for inter-process communication

Create an Angular+Electron app

Let's look at the process of creating an Electron application that loads an Angular application.

The basic idea is:

  1. Create an Angular application like normal with Angular CLI
  2. Create a new JavaScript file that will initialize Electron and have it load the built Angular project index.html
  3. Update the build configuration and scripts to build and run the Electron app.

Create an Angular project

First, make sure you have the Angular CLI tool installed globally so you can use it to generate the Angular project. Then, generate an Angular app with your desired configuration. Then inside your project, install TypeScript and Electron as development dependencies.

npm install -g @angular/cli

ng new angproject --skipGit=true --style=scss --routing=true
cd angproject

npm install --save-dev typescript electron

Now you have a regular Angular project ready for development.

Add Electron

To turn it in to an Electron application, you need to create a TypeScript file that will initialize the Electron browser window and load the index.html file built by Angular.

For this example, I want to create a separate directory to store the backend code. Create a directory named src-backend/ and create a file named main.ts with the TypeScript contents below.

mkdir src-backend
vim src-backend/main.ts # Add the contents below
// src-backend/main.ts
import { app, BrowserWindow } from "electron";
import * as path from "path";

let mainWindow: Electron.BrowserWindow;

app.on("ready", () => {
    mainWindow = new BrowserWindow({
        icon: path.join(__dirname, "../dist/angproject/assets/icon.png"),
        webPreferences: {
            nodeIntegration: true, // Allows IPC and other APIs
        }
    });
    mainWindow.loadFile(path.join(__dirname, "../dist/angproject/index.html"));
});

app.on("window-all-closed", () => {app.quit()});

The code above is a very simple version for demonstration purposes. You should refer to the source in the TypeScript Quickstart Template for a more robust example.

Update build scripts

After creating the main entry file that will load the Electron browser window, the build configuration needs to be modified. Start by modifying package.json to specify the main entry file and update the npm scripts with new build and start scripts.

Add these to the scripts section of package.json:

// package.json scripts section
"build": "ng build --prod --base-href ./ && tsc --lib ES2018,DOM --target ES5 src-backend/main.ts --outDir dist",
"start": "electron .",

Then add this to the top-level JSON object in package.json:

// package.json top-level option
"main": "dist/main.js",

Also modify tsconfig.json to change the target to es5. If you don't do this, the browser window will only show a blank page with the error message: Failed to load module script: The server responded with a non-JavaScript MIME type of "". Strict MIME type checking is enforced for module scripts per HTML spec.

// tsconfig.json
"target": "es5",

Build the application

Now that the npm scripts and build configuration is ready, the application can be built.

npm run build

Run the application

After everything is built, you can run the application.

npm start

Now you have an Electron application that loads your built Angular code. Keep in mind when referencing things like images and other static files, place your static content in src/assets/ and create links like this:

<img src="assets/icon.png" />

Use Angular just like you normally would from this point forward. Use Electron APIs and IPC as needed. Examples of using IPC are included in sections below.

Package the application

You can use https://github.com/electron/electron-packager to build a platform-specific application launcher. For example, in Windows, it will build a .exe and a directory with only the necessary files to run. You can use this to distribute your application other people or build an MSI installer.

First, install the electron-packager module as a development dependency, and then modify package.json npm scripts to include a package command. The package command will run the packager on the proejct directory and output it to the directory/name specified. In this example I include many options like --overwrite to disable the prompt when rebuilding, --asar to bundle your sources into a single file, --icon to specify the .ico icon file, and a whole series of ignore options to keep out unnecessary files. Ignore uses regular expression not glob statements! You will need to modify the ignore statements to suit your needs, but this example ignores all the unnecessary files from the default Angular project.

npm install --save-dev electron-packager

# Add an npm script to `package.json`
"package": "npm run build && electron-packager . myapp --overwrite --asar --icon=dist/angproject/assets/icon.ico --ignore=^e2e$ --ignore=^src$ --ignore=^src-backend$ --ignore=^.editorconfig$ --ignore=^.gitignore$ --ignore=^angular.json$ --ignore=^browserslist$ --ignore=^karma.conf.js$ --ignore=^package-lock.json$ --ignore=^README.md$ --ignore=^tslint --ignore=^tsconfig"

The package command starts with npm run build && so it will always ensure it gets built before packaging. You can package then by running:

npm run package

I did not specify the --platform or --arch so it defaults to the current platform (Windows in my test). Optionally you can specify --all to build all platforms.

If you need to create a Windows .ico icon file see my tutorial on creating Windows .ico icons.

When my package is built in Windows it takes up about 200MB of disk space and about 80MB of RAM to run the default Angular app.

Live reloading

To get live reloading, you can load the URL of http://localhost:4200/ in the main.ts backend while also running ng serve. For example: mainWindow.loadURL('http://localhost:4200/'); instead of mainWindow.loadFile(path.join(__dirname, "../dist/angproject/index.html"));.

You could use an environment variable to swap out which URL electron loads depending on the environment. Use the built index.html file when it's production and use the localhost:4200 when it's development. See my tutorial about Creating environment files in Angular.

Use Electron APIs

Keep in mind you will also need to have initialized the BrowserWindow in the backend with the nodeIntegration option set to true in order to require the electron module.

There are many features available in the Electron API including:

Refer to the docs for a full list. The one example we will look at here is IPC.

Inter-process communication (IPC)

Since the browser (render) process is sandboxed due to browser security features, you must use inter-process communication (IPC) to send messages between the backend (main) process and the browser. Refer to the Electron IPC documentation for more details.

To use IPC, in the main.ts backend process, require ipcMain from electron. In the main process with ipcMain you can listen for and handle events sent from the browser process using ipcMain.on(). You can also send signals from the backend process to the browser process using mainWindow.webContents.send().

// In the backend process `src-backend/main.ts`
import { app, BrowserWindow, ipcMain } from "electron";

ipcMain.on('my-custom-signal', (event, arg) => {
    console.log('Print to the main process terminal (STDOUT) when signal received from renderer process.');
    console.log(event);
    console.log(arg);
    mainWindow.webContents.send('other-custom-signal', 'message from the backend process');
});

Then on the other end in the browser (render) process, you will need to require the electron module to get access to the ipcRenderer object. Similar to the backend ipcMain, you can send signals from the browser to the backend process using electron.ipcRenderer.send() and listen for events using electron.ipcRenderer.on().

// In the Angular `.component.ts` or `.service.ts` file
const electron = (<any>window).require('electron');

// And in the constructor, configure the listening signals
// For example in the `app.component.ts`
constructor() {
    electron.ipcRenderer.on('other-custom-signal', (event, arg) => {
      console.log('Received acknowledged from backend about receipt of our signal.');
      console.log(event);
      console.log(arg);
    })

    console.log('Sending message to backend.');
    electron.ipcRenderer.send('my-custom-signal', 'hello, are you there?');
}

The BrowserWindow and webContents objects

The BrowserWindow object is an important one to understand.

The BrowserWindow object has several options that can be configured on construction. For example, fullscreen options, size and position, and webPreferences like node integration, whether to allow devtools, and default fonts. The BrowserWindow also has events like close, focus, maximize, minimize, resize, and move.

The webContents object on a browser window also has its own events and methods. For example, the events dom-ready, page-title-updated, did-finish-load and the methods contents.executeJavaScript(), contents.setAutioMuted(), contents.setZoomFactor(), contents.insertText(), contents.capturePage(), contents.printToPDF(), contents.openDevTools().

One method worth special note that was covered in the IPC section is contents.send() used to send a message to the browser process that the render process can listen to with electron.ipcRenderer.on().

System tray icons

It's possible to create a system tray only application that does not even use the BrowserWindow. Check out this example that does not even import the browser and only uses the electron app and system tray menus.

In this example, you technically don't even need Angular at all.

The example is modified from https://electronjs.org/docs/api/tray.

// main.ts
import { app, Tray, Menu } from "electron";
import * as path from "path";

app.on("ready", () => {
    let tray = null // https://electronjs.org/docs/api/tray
    tray = new Tray(path.join(__dirname, "../dist/angproject/assets/icon.png"))
    const contextMenu = Menu.buildFromTemplate([
        { label: 'Item1', type: 'radio' },
        { label: 'Item2', type: 'radio' },
        { label: 'Item3', type: 'radio', checked: true },
        { label: 'Exit', type: 'normal', click: () => { app.quit() } }
    ])
    tray.setToolTip('This displays when mouse is hovered.')
    tray.setContextMenu(contextMenu) // Overrides 'right-click' event
    tray.on('click', (event, arg) => {
        console.log('Systray was left-clicked.');
    });
    tray.on('double-click', (event, arg) => {
        console.log('Systray was double-clicked.');
    });
});

Conclusion

After following this guide, you should understand how to:

  • Take a Angular application and turn it in to an Electron app
  • Package the Electron app as a desktop application for distribution
  • Use the Electron API for things like IPC
  • Use live reloading when developing

References

Advertisement

Advertisement