Creating a Desktop App with Electron, Typescript, and Angular

Creating a Desktop App with Electron, Typescript, and Angular

Build and deploy Angular apps in one click
Try Buddy for Free

In this article, we'll learn how to build a cross platform desktop app for Windows, Linux and macOS using web technologies such as Angular, Electron, HTML and TypeScript.

Success

Prerequisites:

  • Familiarity with the three pillars of the web, i.e JavaScript, CSS and HTML
  • A working knowledge of Angular as we'll be using it to build our Angular application along with Electron
  • Git, Node.js and NPM installed on your system

What is Electron

Electron was initially developed for GitHub's Atom editor. Nowadays, it's being used by big companies like Microsoft and Slack to power their desktop apps. Visual Studio Code is a powerful and popular code editor built by Microsoft using Electron.

Hint
You can check out more apps built with Electron from this link.

What Electron precisely does?

So you are a frontend web developer - you know JavaScript, HTML and CSS which is great but you need to build a desktop application. Thanks to Electron that's now possible and you don't have to learn classic programming languages like C++ or Java to build your application, you can simply use your web development skills to target all the popular desktop platforms such as macOS, Linux and Windows with one code base. You only need to rebuild your code for each target platform.

Electron simply provides a native container for your web application, so it looks and feels like a desktop application. If you are familiar with hybrid mobile development, Electron is quite similar to Apache Cordova but targets desktop systems instead of mobile operating systems.

Electron is actually an embedded web browser (Chromium) bundled with Node.js and a set of APIs for interfacing with the underlying operating system and providing the services that are commonly needed by native desktop apps such as:

What is an Electron app?

An Electron app is a desktop application that is built using web technologies such as HTML, CSS, and JavaScript. It allows developers to create cross-platform applications using familiar web development tools. Electron apps have access to the full capabilities of the operating system, making it possible to create applications with native-like features and functionalities. They can interact with the file system, utilize system-level APIs, display native notifications, and more.

Let's now see how we can use Electron and web technologies (TypeScript and Angular) to create a desktop app.

Installing Angular CLI 8

We are going to build an Angular app and wrap it with Electron. The Angular team provides a command line interface that can be used to quickly scaffold Angular projects and work with them locally.

The Angular CLI is based on Node, so provided that you have Node and NPM installed on your machine, you can simply run the following command to install the CLI:

bash
$ npm install -g @angular/cli $$
Hint
At the time of this writing, Angular CLI v8.1.0 was installed on the system.

Creating a project

Let's now create a project. In your terminal, run the following command:

bash
$ ng new electron-app $$

The CLI will prompt you Would you like to add Angular routing? (Y/N). Yype Y if you want to add routing and N otherwise. You will also be asked for Which stylesheet format would you like to use? Let's keep it simple and choose CSS.

That's it, the CLI will create a folder structure with the necessary files for configuring and bootsrapping your project and start installing the dependencies from npm.

You can make sure your application works as expected by serving it locally using the following commands:

bash
$ cd electron-app $ ng serve $$$

You can see your app up and running by going to the http://localhost:4200 address in your web browser. You should see the following interface:

Image loading...The interface of the project

Tip
You can use Buddy to configure a pipeline that builds and deploys Angular apps on a push to Git. Image loading...Example Angular pipeline

Let's now see how we can turn this simple web app into a desktop application using Electron.

Installing & setting up Electron

After creating the project for Angular, Electron can now be installed using the following commands:

bash
$ npm install --save-dev electron@latest $$

This will install Electron as a development dependency in your project.

Hint
As of this writing, Electron v5.0.6 is installed.

With Electron installed, create an app.js file inside the root folder of your project and add the following content:

javascript
const {app, BrowserWindow} = require('electron') const url = require("url"); const path = require("path"); let mainWindow function createWindow () { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } }) mainWindow.loadURL( url.format({ pathname: path.join(__dirname, `/dist/electron-app/index.html`), protocol: "file:", slashes: true }) ); // Open the DevTools. mainWindow.webContents.openDevTools() mainWindow.on('closed', function () { mainWindow = null }) } app.on('ready', createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (mainWindow === null) createWindow() })

We simply create a GUI window using the BrowserWindow API provided by Electron and we call the loadURL() method to load the index.html file from the dist folder.

Hint
At this point, if you look at your project's folder you will not find a dist folder because, we haven't built our Angular project yet.

According to the docs BrowserWindow can be used to create and control browser windows. You can use the backgroundColor property to set the background color. You can invoke BrowserWindow multiple times in your application to create multiple windows and use the parent property to add parent-child relationships between the various windows and also create modals.

Tip
If you want to create a window without Chrome, or a transparent window with an arbitrary shape, you need to use the Frameless Window API instead.

Next, you need to open the package.json file and add the app.js file as the main entry point of our project:

json
{ "name": "electron-app", "version": "0.0.0", "main": "app.js", // [...] }
Hint
You can find more information about the main key in the official docs.

Next, let's modify the start script in the package.json file to make it easy to build the project and run the Electron application with one command:

javascript
{ "name": "electron-app", "version": "0.0.0", "main": "app.js", "scripts": { "ng": "ng", "start": "ng build --base-href ./ && electron .", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, // [...] }

Now, if we run the start script, our project will be built with base href set to ./. Then, our Electron app will run from the current folder.

Hint
The base href attribute is used to set the base URL that will be used for the relative links in the document.

We can test our application by running the following command:

bash
$ npm start $$

The Angular project will be built inside the dist/electron-app folder and Electron will be started with a GUI window that should display the Angular application.

Since we are using Angular 8, the latest version as of this writing, which makes use of differential loading by default - A feature that enables the web browser to make a choice between loading modern (ES6+) or legacy JavaScript (ES5) source files based on its own capabilities. After compiling the Angular application, we'll have two builds, a modern build (es2015) and a legacy build (es5).

If you look at the console of your Electron application, you'll see that it tries to load the modern bundles which gives us the Failed to load module script error.

This is a screenshot of our application with the error displayed in the console:

Image loading...Screenshot of our application with the error displayed in the console

One way to solve this issue is by instructing the TypeScript compiler to generate only a legacy ES5 build. Open the tsconfig.json file and change target from es2015 to es5:

json
{ "compilerOptions": { "target": "es5", }

Now, restart your application, you should see your Angular app loaded inside Electron:

Image loading...Angular Electron app

Calling Electron APIs by example

Let's now see how we can call Electron APIs to invoke the services from the underlying system. We'll see how we can take screenshots of our desktop windows using this example.

Electron use two processes - The main process which runs the Node.js runtime and a renderer process that runs the Chromium browser. For most APIs, we need to use inter-process communication to call the Electron APIs from the renderer process so we'll make use of ngx-electron - A simple Angular wrapper for the Electron's Renderer API which makes calling Electron APIs from the renderer process much easier.

Hint
ngx-electron is a third-party package that makes calling Electron APIs from Angular more straightforward, but you can also use the ipcMain module for implementing asynchronous communication from the main process to renderer processes.

According to the docs the ipcMain module is an instance of the EventEmitter class. When used in the main process, it handles asynchronous and synchronous messages sent from a renderer process (web page). Messages sent from a renderer will be emitted to this module.

Electron provides many useful APIs. Among them, desktopCapturer which allows us to access information about media sources that can be used to capture audio and video from the desktop using the navigator.mediaDevices.getUserMedia API.

In your terminal, run the following command:

bash
$ npm install ngx-electron --save $$

Next, open the src/app/app.module.ts file and add NgxElectronModule to the imports array:

js
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { NgxElectronModule } from 'ngx-electron'; // [...] @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, NgxElectronModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

Next, you can import and inject ElectronService in your component(s) to call Electron APIs.

Open the src/app/app.component.ts file, import ElectronService, and inject it via the component constructor:

js
import { ElectronService } from 'ngx-electron'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit{ title = 'electron-app'; constructor(private _electronService: ElectronService) { } }

Next, let's define the following variables:

js
export class AppComponent implements OnInit{ sources = []; selectedSource; videostream; @ViewChild('videoElement', { static: true }) videoElement: any; video: any;

We'll use:

  • the sources array to store the sources returned from the desktopCapturer API,
  • the selectedSource variable to store a source that we want to take a screenshot for,
  • the videostream variable to store the video stream from the webkitGetUserMedia() API,
  • the videoElement variable decorated with @ViewChild to create a query configuration that will be used to query for the <video> tag.
  • the video variable to store the native DOM element for the <video> tag.

Next, in the ngOnInit() method, add the following code which assigns the native DOM element of the <video> tag referenced by the videoElement reference to the video variable:

js
ngOnInit(){ this.video = this.videoElement.nativeElement; }
Hint
We shall add the <video> tag in the HTML template of the component in the next paragraphs. This tag will be used by the webkitGetUserMedia() to capture the video from the desktop.

Next, let's define the displaySources() which populates the sources array with the available sources from the desktopCapturer API:

js
displaySources(){ if (this._electronService.isElectronApp) { this._electronService.desktopCapturer.getSources({ types: ['window', 'screen'] }, (error, sources) => { if (error) throw error; this.sources = sources; if(this.sources.length > 0){ this.selectedSource = this.sources[0]; } }); } }
  1. First, we use the isElectronApp() method of ElectronService to ensure sure our app is running inside the Electron container and not inside a regular web browser.
  2. Next, we call the getSources() method of desktopCapturer to get the sources.
  3. If there is no error, we assign the sources to the sources variable we previously defined in our component.
  4. We also assign the first source in the array to the selectedSource variable.

Let's define the selectSource() method which selects a source that will be used to take a screenshot:

js
selectSource(source){ this.selectedSource = source; }
Hint
This method simply assigns the selected source to the selectedSource variable.

Finally, let's add the takeScreenshot() variable which will be called to actually take a screenshot of the selected source:

js
takeScreenshot(){ let nav = <any>navigator; nav.webkitGetUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: this.selectedSource.id, minWidth: 1280, maxWidth: 1280, minHeight: 720, maxHeight: 720 } } }, (stream) =>{ this.video.srcObject = stream; this.video.onloadedmetadata = (e) => this.video.play() }, ()=>{ console.log('getUserMediaError'); }); return; }
  1. We call the webkitGetUserMedia() method of the navigator object to capture the video stream from the desktop media source.
  2. We specify the target window using the chromeMediaSourceId property.
  3. After getting the stream, we use the srcObject property to set the stream as the source of the <video> tag.
  4. When the metadata of the video is loaded, we play the video by calling the play() method.

Now, let's add the UI. Open the src/app/app.component.html, remove the existing content, and add the following code:

html
<!--The content below is only a placeholder and can be replaced.--> <div style="text-align: center;"> <h1> Take Screenshots </h1> <button (click)="takeScreenshot()"> Take it! </button> <button (click)="displaySources()"> Sources </button> </div> <ul> <li *ngFor="let source of sources"> <a (click)="selectSource(source)"> {{ source.name }} </a> </li> </ul> <video autoplay width="640" height="480" #videoElement></video>
  1. First we create two buttons for displaying the sources of the screenshots and taking a screenshot of the selected source.
  2. Next, we bind them to the takeScreenshot() and displaySources() methods using the click event.
  3. In the following step, we add a list and iterate over the available sources using the ngFor directive, then we display the name of each source.
  4. We also bind the selectSource() method to each source which enables us to select the source of our screenshot.
  5. In the end, we add a <video> tag that allows us to display the taken screenshot, and we use the template variable reference (called #videoElement) to identify the tag so we can query it later in our component.

This is a screenshot of our Electron app:

Image loading...Screenshot of our Electron app

When we click on the Sources button, the available sources are displayed. We can select any source and then click on the Take it! button to take a screenshot.

The screenshot will be displayed on the <video> tag below the sources.

Hint
This is a simple example that demonstrates how to access Electron APIs from a renderer process running an Angular application. You can improve this application to allow the user to save screenshots using HTML canvas, and add other features that go beyond the intent of our example which is demonstrating how to call Electron APIs from the renderer process.

Packaging your Electron app

After creating your Angular Electron application, you need to package it before you can distribute it to your users.

You can one of the following packaging tools to package your app:

These tools will help you to create a final distributable Electron application, by packaging your application, rebranding the executable, setting icons and also creating the installers for your target systems.

Conclusion

In this tutorial, we've seen what's Electron and how we can use it to create a cross platform desktop app for Linux, macOS and Windows. We've also seen how we can integrate Angular 8 with Electron and used the desktopCapturer API to create a simple screenshot tool from scratch using web technologies only.


Additional resources

Read similar articles