Web applications are traditionally built with HTML, CSS, and JavaScript. Their use has also been widely spread to server development using Node.js. Various tools and frameworks have emerged in recent years that use HTML, CSS, and JavaScript to create applications for desktop and mobile. In this chapter, we are going to investigate how to create desktop applications using Angular and Electron.
Electron is a JavaScript framework that is used to build native desktop applications with web technologies. If we combine it with the Angular framework, we can create fast and highly performant web applications. In this chapter, we will build a desktop WYSIWYG editor and cover the following topics:
Electron is a cross-platform framework that is used to build desktop applications for Windows, Linux, and Mac. Many popular applications are built with Electron, such as Visual Studio Code, Skype, and Slack. The Electron framework is built on top of Node.js and Chromium. Web developers can leverage their existing HTML, CSS, and JavaScript skills to create desktop applications without learning a new language such as C++ or C#.
Tip
Electron applications have many similarities with PWA applications. Consider building an Electron application for scenarios such as advanced filesystem manipulation or when you need a more native look and feel for your application. Another use case is when you are building a complementary tool for your primary desktop product and you want to ship them together.
An Electron application consists of two processes:
An Electron application can have only one main process that can communicate with one or more renderer processes. Each renderer process operates in complete isolation from the others.
The Electron framework provides the ipcMain and ipcRenderer interfaces, which we can use to interact with these processes. The interaction is accomplished using Inter-Process Communication (IPC), a mechanism that exchanges messages securely and asynchronously over a common channel via a Promise-based API.
In this project, we will build a desktop WYSIWYG editor that keeps its content local to the filesystem. Initially, we will build it as an Angular application using ngx-wig, a popular WYSIWYG Angular library. We will then convert it to a desktop application using Electron and learn how to sync content between Angular and Electron. We will also see how to persist the content of the editor into the filesystem. Finally, we will package our application as a single executable file that can be run in a desktop environment.
Build time: 1 hour.
The following software tools are required to complete this project:
We will kick off our project by creating a WYSIWYG editor as a standalone Angular application first. Use the Angular CLI to create a new Angular application from scratch:
ng new my-editor --defaults
We pass the following options to the ng new command:
A WYSIWYG editor is a rich text editor, such as Microsoft Word. We could create one from scratch using the Angular framework, but it would be a time-consuming process, and we would only re-invent the wheel. The Angular ecosystem contains a wide variety of libraries to use for this purpose. One of them is the ngx-wig library, which has no external dependencies, just Angular! Let's add the library to our application and learn how to use it:
npm install ngx-wig
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxWigModule } from 'ngx-wig';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
NgxWigModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
NgxWigModule is the main module of the ngx-wig library.
ng generate component editor
<ngx-wig placeholder="Enter your content"></ngx-wig>
NgxWigModule exposes a set of Angular services and components that we can use in our application. The main component of the module is the ngx-wig component, which displays the actual WYSIWYG editor. It exposes a collection of input properties that we can set, such as the placeholder of the editor. You can find more options at https://github.com/stevermeister/ngx-wig.
<app-editor></app-editor>
html, body {
margin: 0;
width: 100%;
height: 100%;
}
.ng-wig, .nw-editor-container, .nw-editor {
display: flex !important;
flex-direction: column;
height: 100% !important;
overflow: hidden;
}
Let's see what we have achieved so far. Run ng serve and navigate to http://localhost:4200 to preview the application:
Our application consists of the following:
We now have created a web application using Angular that features a fully operational WYSIWYG editor. In the following section, we will learn how to convert it into a desktop one using Electron.
The Electron framework is an npm package that we can install using the following command:
npm install -D electron
The previous command will install the latest version of the electron npm package into the Angular CLI workspace. It will also add a respective entry into the devDependencies section of the package.json file of our project.
Important Note
Electron is added to the devDependencies section of the package.json file because it is a development dependency of our application. It is used only to prepare and build our application as a desktop one and not during runtime.
Electron applications run on the Node.js runtime and use the Chromium browser for rendering purposes. A Node.js application has at least a JavaScript file, usually called index.js or main.js, which is the main entry point of the application. Since we are using Angular and TypeScript as our development stack, we will start by creating a respective TypeScript file that will be finally compiled to JavaScript:
Tip
We can think of our application as two different platforms. The web platform is the Angular application, which resides in the srcapp folder. The desktop platform is the Electron application, which resides in the srcelectron folder. This approach has many benefits, including that it enforces the separation of concerns in our application and allows each one to develop independently from the other. From now on, we will refer to them as the Angular application and the Electron application.
import { app, BrowserWindow } from 'electron';
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
});
We first import the BrowserWindow and app artifacts from the electron npm package. The BrowserWindow class is used to create a desktop window for our application. We define the window dimensions, passing an options object in its constructor that sets the width and height values of the window. We then call the loadFile method, passing as a parameter the HTML file that we want to load inside the window.
Important Note
The index.html file that we pass in the loadFile method is the main HTML file of the Angular application. It will be loaded using the file:// protocol, which is why we removed the base tag in the Adding a WYSIWYG Angular library section.
The app object is the global object of our desktop application, just like the window object on a web page. It exposes a whenReady promise that, when resolved, means we can run any initialization logic for our application, including the creation of the window.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"importHelpers": false
},
"include": [
"**/*.ts"
]
}
The main.ts file needs to be compiled to JavaScript because browsers currently do not understand TypeScript. The compilation process is called transpilation and requires a TypeScript configuration file. The configuration file contains options that drive the TypeScript transpiler, which is responsible for the transpilation process.
The preceding TypeScript configuration file defines the path of the Electron source code files using the include property and sets the importHelpers property to false.
Important Note
If we enable the importHelpers flag, it will include helpers from the tslib library into our application, resulting in a larger bundle size.
npm install -D webpack-cli
The webpack CLI is used to invoke webpack, a popular module bundler, from the command line. We will use webpack to build and bundle our Electron application.
npm install -D ts-loader
The ts-loader library is a webpack plugin that can load TypeScript files.
We have now created all the individual pieces needed to convert our Angular application into a desktop one using Electron. We only need to put them together so that we can build and run our desktop application. The main piece that orchestrates the Electron application is the webpack configuration file that we need to create in the root folder of our Angular CLI workspace:
webpack.config.js
const path = require('path');
const src = path.join(process.cwd(), 'src', 'electron');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: path.join(src, 'main.ts'),
output: {
path: path.join(process.cwd(), 'dist', 'my-editor'),
filename: 'shell.js'
},
module: {
rules: [
{
test: /.ts$/,
loader: 'ts-loader',
options: {
configFile: path.join(src, 'tsconfig.json')
}
}
]
},
target: 'electron-main'
};
The preceding file configures webpack in our application using the following options:
webpack now contains all information needed to build and bundle the Electron application. On the other hand, the Angular CLI takes care of building the Angular application. Let's see how we can combine them and run our desktop application:
npm install -D concurrently
The concurrently library enables us to execute multiple processes concurrently. In our case, it will enable us to run the Angular and Electron applications in parallel.
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration
development",
"test": "ng test",
"start:desktop": "concurrently "ng build --delete-
output-path=false --watch" "webpack --watch""
}
The start:desktop script builds the Angular application using the ng build command of the Angular CLI and the Electron application using the webpack command. Both applications run in watch mode using the --watch option, so that every time we make a change in the code, the application will rebuild to reflect the change. Whenever we modify the Angular application, the Angular CLI will delete the dist folder by default. We can prevent this behavior using the --delete-output-path=false option because the Electron application is also built in the same folder.
Important Note
We did not pass the webpack configuration file to the webpack command because it assumes the webpack.config.js filename by default.
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron"
The runtimeExecutable property defines the absolute path of the Electron executable.
We are now ready to run our desktop application and preview it. Run the following npm command to build the application:
npm run start:desktop
The previous command will build first the Electron application and then the Angular one. Wait for the Angular build to finish and then press F5 to preview the application:
In the preceding screenshot, we can see that our Angular application with the WYSIWYG editor is hosted inside a native desktop window. It contains the following characteristics that we usually find in desktop applications:
The Angular application is rendered inside the Chromium browser. To verify that, click on the View menu item and select the Toggle Developer Tools option.
Well done! You have successfully managed to create your own desktop WYSIWYG editor. In the following section, we will learn how to interact between Angular and Electron.
According to the specifications of the project, the content of the WYSIWYG editor needs to be persisted in the local filesystem. Additionally, the content will be loaded from the filesystem upon application startup.
The Angular application handles any interaction between the WYSIWYG editor and its data using the renderer process, whereas the Electron application manages the filesystem with the main process. Thus, we need to establish an IPC mechanism to communicate between the two Electron processes as follows:
Let's start with the first one, to set up the Angular CLI project for supporting the desired communication mechanism.
We need to modify several files to configure the workspace of our application:
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.loadFile('index.html');
}
The preceding flag will enable Node.js in the renderer process and expose the ipcRenderer interface, which we will need for communicating with the main process.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"electron"
]
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}
The Electron framework includes types that we can use in our Angular application.
import { InjectionToken } from '@angular/core';
export const WINDOW = new
InjectionToken<Window>('Global window object', {
factory: () => window
});
export interface ElectronWindow extends Window {
require(module: string): any;
}
The Electron framework is a JavaScript module that can be loaded from the global window object of the browser. We use the InjectionToken interface to make the window object injectable so that we can use it in our Angular components and services. Additionally, we use a factory method to return it so that it is easy to replace it in platforms with no access to the window object, such as the server.
Electron is loaded using the require method of the window object, which is available only in the Node.js environment. To use it in an Angular application, we create the ElectronWindow interface that extends the Window interface by defining that method.
The Angular and Electron applications are now ready to interact with each other using the IPC mechanism. Let's start implementing the necessary logic in the Angular application first.
The Angular application is responsible for managing the WYSIWYG editor. The content of the editor is kept in sync with the filesystem using the renderer process of Electron. Let's find out how to use the renderer process:
ng generate service editor
import { Inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { ElectronWindow, WINDOW } from './window';
@Injectable({
providedIn: 'root'
})
export class EditorService {
constructor(@Inject(WINDOW) private window:
ElectronWindow) {}
}
private get ipcRenderer(): Electron.IpcRenderer {
return this.window.require('electron').ipcRenderer;
}
The electron module is the main module of the Electron framework that gives access to various properties, including the main and the renderer process. We also set the type of the ipcRenderer property to Electron.IpcRenderer, which is part of the built-in types of Electron.
getContent(): Promise<string> {
return this.ipcRenderer.invoke('getContent');
}
We use the invoke method of the ipcRenderer property, passing the name of the communication channel as a parameter. The result of the getContent method is a Promise object of the string type since the content of the editor is raw text data. The invoke method initiates a connection with the main process through the getContent channel. In the Interacting with the filesystem section, we will see how to set up the main process for responding to the invoke method call in that channel.
setContent(content: string) {
this.ipcRenderer.invoke('setContent', content);
}
The setContent method calls the invoke method of the ipcRenderer object again but with a different channel name. It also uses the second parameter of the invoke method to pass data to the main process. In this case, the content parameter will contain the content of the editor. We will see how to configure the main process for handling data in the Interacting with the filesystem section.
import { Component, OnInit } from '@angular/core';
import { EditorService } from '../editor.service';
@Component({
selector: 'app-editor',
templateUrl: './editor.component.html',
styleUrls: ['./editor.component.css']
})
export class EditorComponent implements OnInit {
myContent = '';
constructor(private editorService: EditorService) {}
ngOnInit(): void {
}
}
ngOnInit(): void {
this.getContent();
}
private async getContent() {
this.myContent = await
this.editorService.getContent();
}
We use the async/await syntax, which allows the synchronous execution of our code in promise-based method calls.
saveContent(content: string) {
this.editorService.setContent(content);
}
<ngx-wig placeholder="Enter your content"
[ngModel]="myContent"
(contentChange)="saveContent($event)"></ngx-wig>
We use the ngModel directive to bind the model of the editor to the myContent component property, which will be used to display the content initially. We also use the contentChange event binding to save the content of the editor whenever it changes, that is, while the user types.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxWigModule } from 'ngx-wig';
import { AppComponent } from './app.component';
import { EditorComponent } from './editor/editor.component';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
EditorComponent
],
imports: [
BrowserModule,
FormsModule,
NgxWigModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We have now implemented all the logic for our Angular application to communicate with the main process. It is now time to implement the other end of the communication mechanism, the Electron application, and its main process.
The main process interacts with the filesystem using the fs Node.js library, which is built into the Electron framework. Let's see how we can use it:
import { app, BrowserWindow, ipcMain } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
The fs library is responsible for interacting with the filesystem. The path library provides utilities for working with file and folder paths. The ipcMain object allows us to work with the main process of Electron.
const contentFile = path.join(app.getPath('userData'), 'content.html');
The file that keeps the content of the editor is the content.html file that exists inside the reserved userData folder. The userData folder is an alias for a special purpose system folder, different for each OS, and it is used to store application-specific files such as configuration. You can find more details about the userData folder as well as other system folders at https://www.electronjs.org/docs/api/app#appgetpathname.
Important Note
The getPath method of the app object works cross-platform and is used to get the path of special folders such as the home directory of a user or the application data.
ipcMain.handle('getContent', () => {
if (fs.existsSync(contentFile)) {
const result = fs.readFileSync(contentFile);
return result.toString();
}
return '';
});
When the main process receives a request in this channel, it uses the existsSync method of the fs library to check whether the file with the content of the editor exists already. If it exists, it reads it using the readFileSync method and returns its content to the renderer process.
ipcMain.handle('setContent', ({}, content: string) => {
fs.writeFileSync(contentFile, content);
});
In the preceding snippet, we use the writeFileSync method of the fs library to write the value of the content property in the file.
"@types/node": "^15.6.0"
Now that we have connected the Angular and the Electron application, it is time to preview our WYSIWYG desktop application:
Congratulations! You have enriched your WYSIWYG editor by adding persistence capabilities to it. In the following section, we will take the last step toward creating our desktop application, and we will learn how to package it and distribute it.
Web applications are usually bundled and deployed to a web server that hosts them. On the other hand, desktop applications are bundled and packaged as a single executable file that can be easily distributed. Packaging our WYSIWYG application requires the following steps:
We will look at both of them in more detail in the following sections.
We have already created a webpack configuration file for the development environment. We now need to create a new one for production. Both configuration files will share some functionality, so let's start by creating a common one:
const path = require('path');
const baseConfig = require('./webpack.config');
module.exports = {
...baseConfig,
mode: 'development',
devtool: 'source-map',
output: {
path: path.join(process.cwd(), 'dist', 'my-
editor'),
filename: 'shell.js'
}
};
"start:desktop": "concurrently "ng build --delete-output-path=false --watch" "webpack --config webpack.dev.config.js --watch""
const path = require('path');
const baseConfig = require('./webpack.config');
module.exports = {
...baseConfig,
output: {
path: path.join(process.cwd(), 'dist', 'my-
editor'),
filename: 'main.js'
}
};
The main difference with the webpack configuration file for the development environment is that we changed the filename of the output bundle to main.js. The Angular CLI adds a hashed number in the main.js file of the Angular application in production, so there will be no conflicts. Other things to notice are that mode is set to production by default when we omit it, and the devtool property is missing because we do not want to enable source maps in production mode.
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"start:desktop": "concurrently "ng build --delete-
output-path=false --watch" "webpack --config
webpack.dev.config.js --watch"",
"build:electron": "ng build && webpack --config
webpack.prod.config.js"
}
The build:electron script builds the Angular and Electron application in production mode simultaneously.
We have completed all the configurations needed for packaging our desktop application. In the following section, we will learn how to convert it into a single bundle specific to each operating system.
The Electron framework has a wide variety of tools that are created and maintained by the open source community.
You can see a list of available projects in the Tools section at the following link:
https://www.electronjs.org/community
One of these tools is the electron-packager library, which we can use to package our desktop application as a single executable file for each OS (Windows, Linux, and macOS). Let's see how we can integrate it into our development workflow:
npm install -D electron-packager
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration
development",
"test": "ng test",
"start:desktop": "concurrently "ng build --
delete-output-path=false --watch" "webpack --
config webpack.dev.config.js --watch"",
"build:electron": "ng build && webpack --config
webpack.prod.config.js",
"package": "electron-packager dist/my-editor --
out=dist --asar"
}
In the preceding script, electron-packager will read all files in the dist/my-editor folder, package them, and output the final bundle in the dist folder. The --asar option instructs the packager to archive all files in the ASAR format, similar to a ZIP or TAR file.
{
"name": "my-editor",
"main": "main.js"
}
The electron-packager library requires a package.json file to be present in the output folder and points to the main entry file of the Electron application.
const path = require('path');
const baseConfig = require('./webpack.config');
const CopyWebpackPlugin = require('copy-webpack-
plugin');
module.exports = {
...baseConfig,
output: {
path: path.join(process.cwd(), 'dist', 'my-
editor'),
filename: 'main.js'
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
context: path.join(process.cwd(), 'src',
'electron'),
from: 'package.json'
}
]
})
]
};
We use the CopyWebpackPlugin to copy the package.json file from the srcelectron folder into the distmy-editor folder while building the application in production mode.
npm run build:electron
npm run package
The preceding command will package the application for the OS that you are currently running on, which is the default behavior of the electron-packager library. You can alter this behavior by passing additional options, which you will find in the GitHub repository of the library listed in the Further reading section.
In the preceding screenshot, the my-editor.exe file is the executable file of our desktop application. Our application code is not included in this file but rather in the app.asar file, which exists in the resources folder.
Run the executable file, and the desktop application should open normally. You can take the whole folder and upload it to a server or distribute it by any other means. Your WYSIWYG editor can now reach many more users, such as those that are offline most of the time. Awesome!
In this chapter, we built a WYSIWYG editor for the desktop using Angular and Electron. Initially, we created an Angular application and added ngx-wig, a popular Angular WYSIWYG library. Then, we learned how to build an Electron application and implemented a communication mechanism for exchanging data between the Angular application and the Electron application. Finally, we learned how to bundle our application for packaging and getting it ready for distribution.
In the next chapter, we will learn how to build a mobile photo geotagging application with Angular and Ionic.
Let's take a look at a few practice questions:
Here are some links to build upon what we learned in the chapter:
3.227.0.245