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:
- Create an Angular application like normal with Angular CLI
- Create a new JavaScript file that will initialize Electron and have it load the built Angular project
index.html
- 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