{{announcement.body}}
{{announcement.title}}

Tutorial: Use Angular and Electron to Create a Desktop App

DZone 's Guide to

Tutorial: Use Angular and Electron to Create a Desktop App

In this article, we discuss how to use Angular and Electron to create a desktop application. We then add authentication to it with Okta.

· Web Dev Zone ·
Free Resource

Developing apps using web technologies has certain advantages. For example, you can use various platforms to run the software of your choice, such as JavaScript, HTML, and CSS. However, developing with web technologies also comes with limitations, like the interoperability with an operating system is restricted and web apps can only be accessed through the browser. Whereas for desktop apps, you can access the features of the operating system directly. You can quickly add to a start menu or the dock, and they run inside their own process. What if you can get a†ll the benefits of a desktop app while using a web tool of your choice? With Electron, you can. 

What Is Electron?

Electron is a JavaScript wrapper around a Chromium web browser. An Electron program consists of two independent JavaScript threads. An outer thread that runs within Node and has access to Node’s operating system libraries, such as File System and Process libraries. Then, there is a JavaScript thread that runs within the browser window. This thread has the usual restrictions of web applications. The outer thread and the browser thread can communicate via inter-process communication (IPC) functions provided by Electron.

Chromium is an open-source web browser that is developed by Google and provides the basis for the Chrome browser. It comes with a powerful JavaScript engine, which makes it possible to run all types of modern web applications. You can think of an electron application, just like a normal web application.

In this tutorial, I’ll be showing you how to develop a desktop application using Electron and Angular. The application will be a simple image browser. Angular will be providing the user interface and processing user interactions. The main process will be accessing the file system and reading directory contents. In addition, I will be showing you how to process authentication with Okta.

You may also like: What Is Electron and Why Should We Use it?

Scaffold the Angular Electron App

I will start with the user interface. I will sometimes refer to this part of the application as the client because of its similarity to clients in web applications. You will hopefully be somewhat familiar with JavaScript and Node. I am assuming that you have already installed Node and the npm command line tool. The client will be based on Angular. To this end, you will also need the Angular command line tool. Open a terminal and enter the command:

Shell




x


 
1
npm install -g @angular/cli@7.3.6



This will install the global ng command. If you are on a Unix-like system, Node installs global commands in a directory that is only writeable by super-users. In this case, you have to run the command above using sudo. To create a new Angular application, navigate to a directory of your choice, and issue the following command.

Shell




xxxxxxxxxx
1


 
1
ng new ImageBrowser --routing --style=css



This will create a directory ImageBrowser and initialize it with a base Angular application. To use the Electron library, you will need to install it first. In the terminal, navigate into the ImageBrowser directory and run this command.

Shell




xxxxxxxxxx
1


 
1
npm install --save electron@4.1.0



Build the Angular Application

The application will use a service that encapsulates the interprocess communication with the Node process. This service is created using the command line as follows:

Shell




xxxxxxxxxx
1


 
1
ng generate service images



This should create a new file, src/app/images.service.ts. Open this file and paste the following code inside it:

TypeScript




xxxxxxxxxx
1
24


 
1
import { Injectable } from '@angular/core';
2
import { BehaviorSubject } from 'rxjs';
3
const electron = (<any>window).require('electron');
4
 
          
5
@Injectable({
6
  providedIn: 'root'
7
})
8
export class ImagesService {
9
  images = new BehaviorSubject<string[]>([]);
10
  directory = new BehaviorSubject<string[]>([]);
11
 
          
12
  constructor() {
13
    electron.ipcRenderer.on('getImagesResponse', (event, images) => {
14
      this.images.next(images);
15
    });
16
    electron.ipcRenderer.on('getDirectoryResponse', (event, directory) => {
17
      this.directory.next(directory);
18
    });
19
  }
20
 
          
21
  navigateDirectory(path) {
22
    electron.ipcRenderer.send('navigateDirectory', path);
23
  }
24
}



The Electron browser library is imported using a somewhat strange-looking require statement, const electron = (<any>window).require('electron'); Electron makes itself available to the browser-side JavaScript through the global window variable. Since the TypeScript compiler is not aware of this, window has to be cast to any before accessing the require function.

Electron provides the ipcRenderer object, which implements interprocess communication for the renderer. ipcRenderer.on is used to register listeners for IPC messages. In this application, you are listening to getImagesResponse which will receive an array of image URLs and getDirectoryResponse, which will receive an array of directory names. To send a request to the Node application to navigate to a different directory, ipcRenderer.send is used.

The images and directory arrays are sent to a BehaviorSubject. In this way, any updates can be picked up by an observer. These observers will be defined in the image browser component. Create this component by calling the ng command in the terminal.

Shell




xxxxxxxxxx
1


 
1
ng generate component browser



Now, open src/app/browser/browser.component.ts and paste the code below into the file:

TypeScript




xxxxxxxxxx
1
30


 
1
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
2
import { ImagesService } from '../images.service';
3
 
          
4
@Component({
5
  selector: 'app-browser',
6
  templateUrl: './browser.component.html',
7
  styleUrls: ['./browser.component.css']
8
})
9
export class BrowserComponent implements OnInit {
10
  images: string[];
11
  directory: string[];
12
 
          
13
  constructor(private imageService: ImagesService, private cdr: ChangeDetectorRef) { }
14
 
          
15
  ngOnInit() {
16
    this.imageService.images.subscribe((value) => {
17
      this.images = value;
18
      this.cdr.detectChanges();
19
    });
20
 
          
21
    this.imageService.directory.subscribe((value) => {
22
      this.directory = value;
23
      this.cdr.detectChanges();
24
    });
25
  }
26
 
          
27
  navigateDirectory(path) {
28
    this.imageService.navigateDirectory(path);
29
  }
30
}



The BrowserComponent subscribes to images and directory of the ImagesService. Note that the changes triggered by an Electron IPC call are not seen by Angular’s change detection strategy. For this reason, a call to ChangeDetectorRef.detectChanges() is needed to tell Angular to update the view with any data changes that might have occurred. Next, open src/app/browser/browser.component.html and create the template for the browser component.

HTML




xxxxxxxxxx
1
14


 
1
<div class="layout">
2
  <div class="navigator">
3
    <ul>
4
      <li *ngFor="let dir of directory">
5
        <a (click)="navigateDirectory(dir)">{{dir}}</a>
6
      </li>
7
    </ul>
8
  </div>
9
  <div class="thumbnails">
10
    <div *ngFor="let img of images" class="image">
11
      <img [src]="img">
12
    </div>
13
  </div>
14
</div>



This template simply displays a list of directories next to a grid of images. When a directory link is clicked, the application requests to navigate to that directory. The browser should also get some styling from src/app/browser/browser.component.css.

CSS




xxxxxxxxxx
1
37


 
1
.layout { display: flex; }
2
 
          
3
.navigator {
4
    width: 300px;
5
    overflow: auto;
6
    flex-grow: 0;
7
    flex-shrink: 0;
8
    border-right: 1px solid #EEEEEE;
9
}
10
 
          
11
.navigator ul { list-style: none; }
12
 
          
13
.navigator a {
14
  cursor: pointer;
15
  font-family: "Courier New", monospace;
16
  font-size: 14px;
17
}
18
 
          
19
.thumbnails {
20
    flex-grow: 1;
21
    display: flex;
22
    flex-wrap: wrap;
23
}
24
 
          
25
.thumbnails .image {
26
    width: 25%;
27
    flex-shrink: 0;
28
    height: 200px;
29
    padding: 8px;
30
    box-sizing: border-box;
31
}
32
 
          
33
.thumbnails img {
34
    width: 100%;
35
    height: 100%;
36
    object-fit: cover;
37
}



To show the browser component as the main component of the application, modify src/app/app-routing.module.ts to import the component and include it as the main route in the routes array.

TypeScript




xxxxxxxxxx
1


 
1
import { BrowserComponent } from './browser/browser.component';
2
 
          
3
const routes: Routes = [
4
  { path: '', component: BrowserComponent }
5
];



Next, open src/app/app.component.html and delete everything except the router outlet.

HTML




xxxxxxxxxx
1


 
1
<router-outlet></router-outlet>



Finally, open src/app/app.component.ts and modify the contents to match the code below.

TypeScript




xxxxxxxxxx
1
17


 
1
import { Component, OnInit } from '@angular/core';
2
import { ImagesService } from './images.service';
3
 
          
4
@Component({
5
  selector: 'app-root',
6
  templateUrl: './app.component.html',
7
  styleUrls: ['./app.component.css'],
8
})
9
export class AppComponent implements OnInit {
10
  title = 'Image Browser';
11
 
          
12
  constructor(private imageService: ImagesService) {}
13
 
          
14
  ngOnInit(): void {
15
    this.imageService.navigateDirectory('.');
16
  }
17
}



The application component initializes the image service by loading the contents of the current directory. This completes the client part of the application. As you can see, it is a typical Angular application apart from the fact that the image service communicates via IPC calls. You could extend this application just like any other web application with multiple routes or HTTP calls to other web services.

Create Your Electron Application

The Electron application will be placed into its own directory. For larger applications, you will probably keep the two parts of the application completely separate in different folders. For the sake of simplicity, in this tutorial, the Electron application will be implemented in a subdirectory of our application directory.

Within the ImageBrowser directory, create a new directory electron. Copy the tsconfig.json from the Angular application into this directory. Open the new tsconfig.json and modify the output directory to "outDir": "./dist" and the module resolution to "module": "commonjs". Also, add the setting, "skipLibCheck": true. Now, create a new file, electron/main.ts, and paste the following code into it.

TypeScript




xxxxxxxxxx
1
78


 
1
import { app, BrowserWindow, ipcMain } from "electron";
2
import * as path from "path";
3
import * as url from "url";
4
import * as fs from "fs";
5
 
          
6
let win: BrowserWindow;
7
 
          
8
function createWindow() {
9
  win = new BrowserWindow({ width: 800, height: 600 });
10
 
          
11
  win.loadURL(
12
    url.format({
13
      pathname: path.join(__dirname, `/../../dist/ImageBrowser/index.html`),
14
      protocol: "file:",
15
      slashes: true
16
    })
17
  );
18
 
          
19
  win.webContents.openDevTools();
20
 
          
21
  win.on("closed", () => {
22
    win = null;
23
  });
24
}
25
 
          
26
app.on("ready", createWindow);
27
 
          
28
app.on("activate", () => {
29
  if (win === null) {
30
    createWindow();
31
  }
32
});
33
 
          
34
// Quit when all windows are closed.
35
app.on('window-all-closed', () => {
36
  // On macOS it is common for applications and their menu bar
37
  // to stay active until the user quits explicitly with Cmd + Q
38
  if (process.platform !== 'darwin') {
39
    app.quit()
40
  }
41
});
42
 
          
43
function getImages() {
44
  const cwd = process.cwd();
45
  fs.readdir('.', {withFileTypes: true}, (err, files) => {
46
      if (!err) {
47
          const re = /(?:\.([^.]+))?$/;
48
          const images = files
49
            .filter(file => file.isFile() && ['jpg', 'png'].includes(re.exec(file.name)[1]))
50
            .map(file => `file://${cwd}/${file.name}`);
51
          win.webContents.send("getImagesResponse", images);
52
      }
53
  });
54
}
55
 
          
56
function isRoot() {
57
    return path.parse(process.cwd()).root == process.cwd();
58
}
59
 
          
60
function getDirectory() {
61
  fs.readdir('.', {withFileTypes: true}, (err, files) => {
62
      if (!err) {
63
          const directories = files
64
            .filter(file => file.isDirectory())
65
            .map(file => file.name);
66
          if (!isRoot()) {
67
              directories.unshift('..');
68
          }
69
          win.webContents.send("getDirectoryResponse", directories);
70
      }
71
  });
72
}
73
 
          
74
ipcMain.on("navigateDirectory", (event, path) => {
75
  process.chdir(path);
76
  getImages();
77
  getDirectory();
78
});



Don’t be intimidated by the amount of content you see here. I will talk you through this file step-by-step.

At the top of the file, a global variable, win, is declared. In the following function, createWindow(), this variable is assigned a new BrowserWindowBrowserWindow is Electron’s application window. It is called Browser Window because it really is a simple Chromium browser that will host your Angular application. After win is created, content is loaded into it with win.loadURL(). The path should point to the index.html of compiled Angular app.

The line win.webContents.openDevTools() opens the developer tools inside Chromium. This should be used for development only. But, it allows you to use the full set of developer tools that you are probably familiar with from the Chrome browser.

Next, an event handler is added to the window that is activated when the window is closed, setting the win variable to null. Later on, when the application is activated again, win can be checked and a new window can be created. This is done in the app.on("activate", ...) handler.

The createWindow function is registered with the ready event by calling app.on("ready", createWindow). The window-all-closed event signals that all windows are closed. On most platforms, this should terminate the application. However, on macOS closing the window does not normally terminate the application.

Two functions getImages and getDirectory perform similar operations. They both read the current directory and filter its contents. getImages selects all files ending in .png or .jpg and construct a full URL for each file. It then sends the result to the getImagesResponse IPC channel. This will be received by the ImagesService of the Angular part of the application. 

getDirectory is very similar, but it selects only directories and sends the result to getDirectoryResponse. Note that the file system’s fs.readdir does not return an entry for the parent directory. So, when the current directory is not the root directory, the .. entry is manually added to the list.

Finally, an IPC listener is added that listens to the navigateDirectory event. This listener changes the current directory and then retrieves all images and directories from the new directory.

To run the full application, you can add the following script to your package.json.

JSON




xxxxxxxxxx
1


 
1
"electron": "ng build --base-href ./ && tsc --p electron && electron electron/dist/main.js"



This script first builds the Angular application, then the Electron application, and finally starts Electron. You can run it by calling this command.

Shell




xxxxxxxxxx
1


 
1
npm run electron



If you did everything right, the application should compile, and you should see a window popping up that lets you browse directories and view the images in them.

Add Authentication to Your Angular Electron Desktop App

You may want to restrict access to your desktop application to users that are registered. Okta allows you to quickly set up secure authentication with full user control. This means that you can freely decide who can use your application and who can’t.

To start, you have to register a free developer account with Okta. In your browser, navigate to https://developer.okta.com, follow the sign-in link, fill in the form that appears next, and click on the Get Started button. After you have completed the registration process, you can navigate to your Okta dashboard. Select Applications in the top menu and create your first application. To do this, click on the green button that says “Add Application.”

On the screen that appears next, select Native and click on Next. The next screen allows you to edit the settings. The Login Redirect URI is the location that receives the authentication token after a successful login. This should match the Redirect URI in your application. In this example, set it to http://localhost:8000. When you’re done, click on the Done button. The resulting screen will provide you with a client ID which you need to paste into your application.

Setting redirect URIs

Setting redirect URIs

I will be using the AppAuth library from Google which allows authentication through OIDC and OAuth 2.0. You can install the library with the following command.

Shell




xxxxxxxxxx
1


 
1
npm install --save @openid/appauth@1.2.2



Google provides an example of how to integrate AppAuth with Electron. To make your life simple, you can use the authentication flow for your own application. Copy the contents of the example flow.ts into a flow.ts file in your electron folder. Near the top of the file, find the following lines.

TypeScript




xxxxxxxxxx
1


 
1
/* an example open id connect provider */
2
const openIdConnectUrl = "https://accounts.google.com";
3
 
          
4
/* example client configuration */
5
const clientId =
6
  "511828570984-7nmej36h9j2tebiqmpqh835naet4vci4.apps.googleusercontent.com";
7
const redirectUri = "http://127.0.0.1:8000";
8
const scope = "openid";



Replace them with the following code:

TypeScript




xxxxxxxxxx
1


 
1
const openIdConnectUrl = 'https://{yourOktaDomain}/oauth2/default';
2
const clientId = '{yourClientId}';
3
const redirectUri = 'http://localhost:8000';
4
const scope = 'openid profile offline_access';



To keep the example minimal, replace the import of the logger, import { log } from "./logger"; with const log = console.log;. Now, open electron/main.ts again. At the top of the file, import some classes from flow.ts.

TypeScript




xxxxxxxxxx
1


 
1
import { AuthFlow, AuthStateEmitter } from './flow';



Then, at the bottom of the same file add the following snippet.

TypeScript




xxxxxxxxxx
1
12


 
1
const authFlow = new AuthFlow();
2
 
          
3
authFlow.authStateEmitter.on(
4
    AuthStateEmitter.ON_TOKEN_RESPONSE, createWindow
5
);
6
 
          
7
async function signIn() {
8
  if (!authFlow.loggedIn()) {
9
    await authFlow.fetchServiceConfiguration();
10
    await authFlow.makeAuthorizationRequest();
11
  }
12
}



The function signIn() will check if the user is logged in, and, if not, make an authorization request. The authStateEmitter will receive an ON_TOKEN_RESPONSE when the user is successfully logged in. It will then call createWindow to start the application. In order to call the signIn method, change the handler for the application’s ready event to the following.

TypeScript




xxxxxxxxxx
1


 
1
app.on('ready', signIn);



Give it a try and run the following command.

Shell




xxxxxxxxxx
1


 
1
npm run electron



Your default web browser should open up and request you to log in to your Okta account. Once successfully logged in, the Image Browser application will open up.

Final output

Final output

Learn More About Angular and Electron

In this tutorial, I have shown you how to create a desktop application with Angular and Electron. Authentication control with Okta has been added using Google’s AppAuth library. Electron makes it straightforward to use current web technologies and create native desktop applications. Electron uses the Chromium browser to run a web client. The browser is controlled by a Node process. To learn more about Electron, Angular and authentication, why not check out one of the following links.

The code for this tutorial is available on GitHub and as always, leave your questions or feedback in the comments, or reach out to us on Twitter @oktadev.

Further Reading

Topics:
angular, electron, javascript, oauth, tutorial, user authentication

Published at DZone with permission of Holger Schmitz , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}