Ravendyne Inc

Ang-El on

Electron 11 with Angular 9 and Node 12, from scratch

For Angular 9.x.y we use NodeJS v12.x and for that version of nodejs we need to use electronjs v11.x.y.

$ ng --version

...(snip)...

Angular CLI: 9.1.12
Node: 12.16.1
OS: linux x64

Angular: 
... 
Ivy Workspace: 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.901.12
@angular-devkit/core         9.1.12
@angular-devkit/schematics   9.1.12
@schematics/angular          9.1.12
@schematics/update           0.901.12
rxjs                         6.5.4

Create initial Angular project

Create new Angular app as usual, except DO NOT select to use Angular Routing for now! Angular Router does not play well with browser window reloads in Electron which will make live/hot reload useless. We’ll introduce solution to this later on. One thing at a time :)

$ ng new angular-electron-howto
$ cd angular-electron-howto

Package versions we ended up with after creating the new app:

$ ng --version

...(snip)...

Angular CLI: 9.1.15
Node: 12.16.1
OS: linux x64

Angular: 9.1.13
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.901.15
@angular-devkit/build-angular     0.901.15
@angular-devkit/build-optimizer   0.901.15
@angular-devkit/build-webpack     0.901.15
@angular-devkit/core              9.1.15
@angular-devkit/schematics        9.1.15
@angular/cli                      9.1.15
@ngtools/webpack                  9.1.15
@schematics/angular               9.1.15
@schematics/update                0.901.15
rxjs                              6.5.5
typescript                        3.8.3
webpack                           4.42.0

Create initial Electron app

Next, add electron, ts-node and some utilities we’ll need:

$ npm i -D electron@11-x-y
$ npm i -D ts-node@9
$ npm i -D @types/node@12
$ npm i -D rimraf

Now we set up Electron app inside our Angular project.

Create app-electron folder under src:

$ mkdir src/app-electron

We’ll keep all our Electron related sources in there.

Next, add initial versions of main.ts and preload.ts files to the src/app-electron folder. These can be found in resources folder.

Notice how, when we create new BrowserWindow, we added favicon.png as an app icon:

{
  icon: path.join(__dirname, 'ui/favicon.png'),
}

If you have favicon.ico file and want to use it as your Electron app icon, you can use any of the online services to convert ico to png.

We have also set nodeIntegration and contextIsolation like so:

webPreferences: {
  nodeIntegration: false,
  contextIsolation: true,
}

which are prefered values and we will introduce simple method of communication between Angular frontend and Electron backen later on, that works with these settings.

Also, we changed index.html loading code into this:

  mainWindow.loadFile(path.join(__dirname, "ui/index.html"));

We will be placing all of the Angular built sources under ui folder in the final build output, so we need to change that call to loadFile() accordingly.

To instruct Angular to place built sources to the new location, change outputPath property in angular.json like so:

{
  "projects": {
    "angular-electron-howto": {
      "architect": {
        "build": {
          "options": {
            "outputPath": "dist/ui",
          }
        }
      }
    }
  }
}

and add favicon.png to the existing list of assets in order to be copied to dist/ui folder, like so:

{
  "projects": {
    "angular-electron-howto": {
      "architect": {
        "build": {
          "options": {
            "assets": [
              "src/favicon.png",
            ],
          }
        }
      }
    }
  }
}

This will set the application icon in the taskbar in Linux and Windows. For MacOS we’ll need to do a couple more steps, later.

Next, we need to set up compilation for the Electron part. Here is where we introduce a little trick: instead of setting up our own compilation tools and process from scratch, we will hijack Angular’s server builder to do the work for us.

First, we need to create two tsconfig files: one for the main.ts and all the code that will be executing in Electron Main process, and one for the preload.ts and all the code that will be executing in Electron Renderer process, excluding Angular code. Angular code will be built by its standard browser builder.

Copy tsconfig files from the resources folder and paste them into the project’s root, right alongside with tsconfig.json Angular created for us. We added few properties in the original tsconfig.json at this point, you don’t have to, this app will work fine without these:

{
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true,
  },
  "angularCompilerOptions": {
    "strictTemplates": true,
  }
}

The compilerOptions ones allow us to simply import JSON files in our TypeScript soruce as if they were regular modules. The strictTemplates allows for type checks to be performed in Angular HTML template files which, among other things, provides completion support for variables when you edit them.

Once tsconfig and app-electron files are all in place, make sure all is set up correctly by running tsc compiler:

$ npx tsc -p tsconfig.el.json
$ npx tsc -p tsconfig.elp.json

There should be no output from these commands, the only thing that should change is dist folder should be created with main.js and preload.js files in them (together with corresponding map.js files).

Initial versions of build scripts and first app run

Before we run our Angular/Electron app for the first time, let’s set up a few build scripts and some properties in the project’s package.json file.

First, we’ll add main property like so:

{
  "main": "dist/main.js",
}

That property is added solely so that we can run our interim, development, non-packaged application from the project root folder.

Next, let’s add a few build scripts:

{
  "scripts": {
    "clean": "rimraf dist build",
    "build": "npm run clean && npm run build:angular && npm run build:electron && npm run build:electronp",
    "build:angular": "ng build --base-href ./",
    "build:electron": "tsc -p tsconfig.el.json",
    "build:electronp": "tsc -p tsconfig.elp.json",
  }
}

You can run npm run build to build our app, or run each script one by one to make sure they all work as intended first.

To run our app, add this simple script:

{
  "scripts": {
    "electron": "electron .",
  }
}

and then run it:

$ npm run electron

The app should start and you should see standard default Angular app UI for a new project, Developer Tools console open, and, if on Linux or Windows, our application should show familiar Angular icon in the OS taskbar/applications list.

Live reload for developers

Once we have our app set up and running, and before we switch to using Angular server builder for the Electron part, we will add live reload support, so we don’t have to rebuild/reload every time we change something. For this purpose, we will use Angular’s own live reload support for the UI, and electron-reload package on the Electron side.

Let’s install electron-reload first:

$ npm i -D electron-reload

and then we need to load it and activate reload, but only if we are running in development mode. To achieve this, add this code to src/app-electron/main.ts:

if( ! app.isPackaged ) {
  const electronReload = require("electron-reload");
  electronReload( path.join(__dirname, "."), {
    // to have electron process restarted, if needed
    electron: path.join(app.getAppPath(), 'node_modules', '.bin', 'electron')
  });
}

right after:

import path from "path";

line.

This code will import electron-relaod module, if our application is not packaged, which means we are still in development mode, and then it will instruct electron-reload to reload our app every time anything in the directory where main.js is changes. We also add a path to electron executable from the project’s node_modules/.bin folder so that electron-reload can reload our app when the Electron side (from src/app-electron) code changes.

Let’s first add and test a convenient script to run Angular build in watch mode, which will rebuild the UI every time we make a change to it. Add the script to package.json:

{
  "scripts": {
    "watch:angular": "ng build --base-href ./ --watch",
  }
}

and run it in one terminal:

$ npm run watch:angular

Then open another terminal, and run:

$ npm run build:electron
$ npm run electron

in that one.

Once the app is running, change something on the Angular side of code, i.e. change title in app.component.ts:

title = 'My Ang/El';

UI should reload and you should see “My Ang/El app is running!” text next to the little rocket icon at the top (if you haven’t changed the default Angular app.component.html template laready).

Notice how we had to first run build:electron script before we ran the app itself. In order to not have to do this, we will install watch package:

$ npm i -D watch

and add a script to watch for changes in src/app-electron folder:

{
  "scripts": {
    "watch:electron": "watch 'npm run build:electron && npm run build:electronp' ./src/app-electron",
  }
}

Close your app, if it is still running, and in that second terminal you used to run your app from, execute our new watch:electron script:

$ npm run watch:electron

Then, open a third console (I know…) and run the app in there:

$ npm run electron

Angular UI reloading works, let’s test if Electron side code reloading works by adding simple console.log anywhere in main.ts:

console.log('Reload works!')

and save it.

The watch utility we started in second console should detect a change in src/app-electron folder and run rebuild of both main.ts and preload.ts. It all is set up correctly, electron-reload should detect change of files in dist folder and it should close the current instance of your app and re-open a new one and you should see “Reload works!” message in the terminal you used to start the app (the third terminal, I’d say :) ).

Electron reloading is not as slick as Angular UI reloading, but that’s just the way things work with Electron-side code.

Use Angular server builder to build Electron app

Now we need to set up Angular’s server builder to build the Electron-side source of our app and nicely package it into a single file. Well, two files actually: main.js and preload.js.

To do that, we need to add two build configurations to angular.json and we need to add very simple webpack config file in order to build the preload.ts correctly.

First, add the two build configurations to angular.json under angular.<your-project-name>.architect property:

{
  "projects": {
    "angular-electron-howto": {
      "architect": {

        "electron": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist",
            "main": "src/app-electron/main.ts",
            "tsConfig": "tsconfig.el.json",
            "deleteOutputPath": false,
            "externalDependencies": [
              "electron",
              "electron-reload"
            ]
          },
          "configurations": {
            "production": {
              "outputHashing": "media",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "sourceMap": false,
              "optimization": true
            }
          }
        },

        "electron-preload": {
          "builder": "@angular-builders/custom-webpack:server",
          "options": {
            "customWebpackConfig": {
              "path": "./webpack.config.elp.js",
              "mergeStrategies": {
                "module.rules": "prepend"
              }
            },
            "outputPath": "dist",
            "main": "src/app-electron/preload.ts",
            "tsConfig": "tsconfig.elp.json",
            "deleteOutputPath": false,
            "externalDependencies": [
              "electron",
              "electron-reload"
            ]
          },
          "configurations": {
            "production": {
              "outputHashing": "media",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "sourceMap": false,
              "optimization": true
            }
          }
        }
      }
    }
  }
}

Angular’s Build Architect server target will build our Electron sources, but it will always create an output file named main.js. This is fine for our main.ts file, but it is completely wrong when it comes to compiling preload.ts. One way to build preload.ts file using Angular’s Build Architect, and place it in dist folder as preload.js, is to use @angular-builders/custom-webpack:server builder instead of @angular-devkit/build-angular:server and supply it a simple webpack.config.json file that just specifies output file name: preload.js.

To that end, let’s first install @angular-builders/custom-webpack:

$ npm i -D @angular-builders/custom-webpack@9.x.x

Next, copy webpack.config.elp.json from resources folder to project’s root. That file is referenced by customWebpackConfig property in electron-preload build configuration.

That should be it, so let’s test how our new build setup works. Run:

$ npm run clean
$ npm run build:angular
$ ng run angular-electron-howto:electron
$ ng run angular-electron-howto:electron-preload

We should see main.js and preload.js in the dist folder, as before, but this time they have been nicely webpack-ed for us.

Test-run the app:

$ npm run electron

and it should launch and work as before.

And, as a final step, let’s update our build:electron and build:electronp scripts:

{
  "scripts": {
    "build:electron": "ng run angular-electron-howto:electron",
    "build:electronp": "ng run angular-electron-howto:electron-preload",
  }
}

Next, we’ll add packaging to our project, so we can nicely package and distribuit our application to end users.

Packaging the project

For packaging, we will use electron-packager and will write a very simple script for it.

First, let’s install electron-packager:

$ npm i -D electron-packager

We created a very simple packaging script you can find in resources/tools folder. This script runs electron-packager and instructs it to not include a set of source directories and project files which shouldn’t be included in you distribuition build anyways. Other than that, you can set some other options in there if you need to, but by default packager will build distribuition package for the target OS that you are running the packager on.

First, let’s copy the tools folder from resources folder into project root. You should now have a tools folder in your project root, and there should be build-package.ts and tsconfig.tools.json in the tools folder.

Next, let’s create a build script that will invoke our packaging script, by adding following to the package.json:

{
  "scripts": {
    "build:package": "ts-node -P tools/tsconfig.tools.json tools/build-package.ts",
  }
}

and then re-build our app from scratch:

$ npm run build

and package it:

$ npm run build:package

You should end up with a new folder called build (that you should add to your project’s .gitignore). Maybe build and dist folders should have their names switched :), it’s up to you.

The build folder should contain a sub-folder named like <project name>-<platform name>-<platform arch>. That folder contains packaged build of your application, for that specific platform (i.e. Linux, MacOS, Windows) and that specific architecture (i.e. x86, x64, Arm).

We can test the build app by running it (this is example for 64-bit Linux platform):

$ cd build/angular-electron-howto-linux-x64
$ ./angular-electron-howto

The application should start as usual, except you may notice that Developer Tools is not being open. Also, hot reload will be disabled.

Marvel at you work a bit and then close the app, we have few more things to do before we wrap this up.

Communication between Angular UI and Electron backend

There are many ways to implement communication between Angular UI (renderer process) and Electron backend (main process), and those depend on what you want to achieve and what is you preference when it comes to coding.

We will now implement one possible, and simple, way of interaction between Angular UI and Electron.

These are main guidelines when it comes to what this implementation needs to achieve: - It has to play with the facts that Node integration is disabled and context isolation is enabled, both good security practices - It should mimic the way “normal” Angular Web application usually works: UI makes call to the server, gets response and then does something with it

So, our Angular-Electron interaction, as implemented here, will be driven from the Angular UI side: this means no interaction will be initiated from the Electron side. If you need that, you can implement it using similar pattern to what we will show here, or maybe something else completely.

API design

Communication between UI (aka “renderer process”) and the rest of app (aka “main process”) in Electron-based apps consists of two parts: - the part you implement in renderer process by using ipcRenderer methods - the part you implement in main process by using ipcMain methods

There’s a handful of ways you can implement your renderer-main communication, and we will use the invoke-handle one. That method of renderer-main communication works like this: - you call ipcMain.handle(channel,handler) method somewhere in your code that is ultimately executed by the main.ts, and you provide a callback function handler to that method which will handle all messages sent by renderer process on the channel channel. - whenever you need to call the handler in main process, you call ipcRenderer.invoke(channel,args) method somewhere in your code that is ultimately executed by the preload.ts, you provide channel name and arguments you want to send to the code to be executed in the main process, and wait for the result via Promise.then. - because we are using process isolation and are NOT using Node integration, we also must expose this ipcRenderer.invoke to the UI code by using contextBridge.exposeInMainWorld

If all this sounds a bit complicated to do, especially if you have dozens and dozens of methods in your app, that’s because, well it is a bit complicated. The IPC implemented in Electron needs to support wide variety of use cases, so it needs to be a bit complicated.

However, we are going to use just one way of communication, and in order to make it as simple as we can, we’ll implement something we call API Bridge, and it goes something like this:

On the main process side, you need to call APIBridgeMain.serve function which has this signature:

type MainAPIBridgeHandler = (...args: any[]) => Promise<any>;
interface APIBridgeMain {
  serve( method: string, handler: MainAPIBridgeHandler ): void;
}

This method needs to be called only once, in a code that will ultimately be executed in main.ts. Your handler function can have any number of arguments and it must return a Promise.

In you Angular UI code, every time you need to invoke that handler function you just set up in the main process, you need to call APIBridge.fetch function which has this signature:

interface APIBridge {
  fetch( method: string, ...args: any[] ) : Observable<any>;
}

First parameter to this function is method value that must match method value in the APIBridgeMain.serve call above, and the rest of the parameters must match the list of parameters of your handler function. The Observable that this method returns will emit the value returned by your handler function.

The value you provide for method parameter does not matter as long as it is unique among all of the serve() function calls, and is the same value in both serve() and fetch() calls.

A simple example for illustration:

On the main process side:

APIBridgeMain.serve('my-method', (param:number) => {
  return Promise.resolve(param + 42);
})

On the Angular side:

APIBridge.fetch('my-method', 68).subscribe( result => {
  console.log('result =',result);
})

Whenever Angular side executes above APIBridge.fetch, you should see result = 100 printed in console in Dev Tools window.

This method of communication looks similar to what you would do in a web-based Angular application: you call a service method which fetch-es something from a web server and returns result you then use in you Angular app.

That similarity is not a coincidence :)

Next, let’s move on implementing this serve/fetch abstraction for our Angular/Electron application.

API implementation and setup

Sources for this simple API bridge implementation can be found in resources/api-bridge folder.

Create app/services folder for the Angular services code to be placed into, and creaate app-electron/services folder for the Electron side services code to be placed into. Then, copy files from resources/api-bridge/app/services folder to src/app/services and files from resources/api-bridge/app-electron/services folder into src/app-electron/services.

Let’s first look at the Electron side of things, the app-electron/services folder.

The api-bridge.renderer.interface.ts file exports APIBridgeRendererItnf interface and is set as a separate file just so we can improt it into the Angular side and have a single point of truth for how the API bridge interface actually looks like. This file is referenced from app-electron/services/api-bridge.renderer.ts and app/services/api-bridge.ts files. The first one implements the interface in APIBridgeRenderer class, and the second one uses the interface to type the context bridge variable used to host the API bridge implementation itself.

The app-electron/services/api-bridge.main.ts file implements the API bridge serve method which you can then use in your Electron-side service implementation by calling APIBridgeMain.serve(). You can also use MainAPIBridgeHandler type if you need to declare your Electron-side service handler functions.

The app-electron/services/api-bridge.renderer.ts file implements the API bridge fetch method which returns Promise. This method will be wrapped on Angular side into APIBridge.fetch() method that returns Observable, to keep it in line with coding that uses Angular framework.

As previously said, in order to expose this method to the Angular UI side code, we need to call contextBridge.exposeInMainWorld() function once in Electron code that is executed in renderer context. We call contextBridge.exposeInMainWorld() in setupRemoteAPIBridge() function exported from api-bridge.renderer.ts. This function needs to be called from preload.ts at some point, so let’s add a call to it in our app-electron/preload.ts file:

import { setupRemoteAPIBridge } from './services/api-bridge.renderer';
setupRemoteAPIBridge();

You can add this code anywhere in preload.ts and that is pretty much everything you need to do to set up our simple API Bridge contraption.

Next, let’s use our newly set up API Bridge and implement a very simple service on Electron side and call it from Angular side, just to illustrate how all this works.

API usage and example service

We will now show what you need to do in order to use the API Bridge in your code.

To do that, we will implement the example service we showed above: a service that adds 42 to whatever you pass it as parameter and returns the result of the operation.

First, let’s implement Electron-side code.

We already have coded the code for you, so you can just copy resources/example/app-electron/services/example.service.ts file to src/app-electron/services folder, and resources/example/app/services/example.service.ts to src/app/services folder. Both files are called the same so we know where to look for the Electron side of the Angular service and vice versa, which you may or may not like, feel free to change however best fits you style.

If you look at app-electron/services/example.service.ts file, you’ll notice that we call APIBridgeMain.serve function in the ExampleServiceMain class’ constructor and we use a lambda function for example/method handler, which then calls the method that does the actual work, addFortyTwo() in this case. Reason for this is that, if you specify addFortyTwo() method directly as a handler parameter like this:

  APIBridgeMain.serve( this.exampleMethod, this.addFortyTwo );

you won’t be able to reference any property in ExampleServiceMain class via this keyword, because this will be undefined when ipcMain.handle() calls the handler function. By supplying a lambda like we do, we capture this within the lambda and when the lambda calls addFortyTwo this will be there for addFortyTwo to use.

In this particular case, we actually could have used addFortyTwo directly as handlerm without intermediate lambda, because we don’t reference this anywhere from within addFortyTwo.

If you need to add more methods to your service class and expose them to the Angular UI side, just line up more APIBridgeMain.serve calls in the constructor, making sure that each one has a different channel name, just like we did with the addFortySeven() method.

One last thing for the Electron-side left to do, is to create an instance of our ExampleServiceMain class for two reasons: - so its constructor is called and all the APIBridgeMain.serve calls are executed - so that we have an instance of the class which could keep track of its state through class properties and all that good object-oriented stuff

For this to happen, we will create app-electron/services/index.ts file, which you can copy from resources/example/app-electron/services folder. This file exports one function called setupAPIServicesMain() and this function does nothing else but create one instance for each of your service classes. References to the instances are kept in apiServices variable, each having its own property in there.

setupAPIServicesMain() needs to be calle from main.ts file, so let’s add that too at the end of main.ts:

import { setupAPIServicesMain } from "./services";
setupAPIServicesMain();

From now on, adding a new service’s Electron-side code comes down to writing the service class, calling APIBridgeMain.serve for each method you want to expose and then adding a line in setupAPIServicesMain() which creates an instance of the new service.

The ExampleServiceMain constructor has no parameters which doesn’t mean you shouldn’t add some if you need them, we just didn’t need any in this simple example.

That concludes the Electron-side part, let’s do the Angular side.

As already said, the src/app/services/example.service.ts file contains implementation of the Angular-side of our example service which uses the API Bridge. You can see in there that we are declaring our service as available at Angular application root level:

@Injectable({
  providedIn: 'root'
})

This is just for simplicity sake, and is not important for usage of API Bridge. As you can see, using API Bridge on Angular side is very simple, just call APIBridge.fetch with correct value for method parameter and provide other parameters you need to pass to the Electron-side service that will handle the call.

Make sure to use the correct string value for the method parameter for your fetch calls. We re-declared the constants containing method string values, which is a bad idea. These should be placed in a common file kept either in app or app-electron side and then import-ed by both Electron and Angular service ts files. Also, constants should have better names than exampleMethod and exampleMethodTwo, maybe something like methodAddFortyTwo and methodAddFortySeven or whatever else works best for your coding style.

Again, the value you provide for method parameter does not matter as long as it is unique among all of the serve() function calls, and is the same value in both serve() and fetch() calls.

Having created our Angular service, we should invoke it from UI.

Replace app/app.component.html content with this one:

<button (click)="do42()">forty two</button>
<br>
<button (click)="do47()">forty seven</button>

and add constructor and the missing methods to app/app.component.ts file:

import { ExampleService } from './services/example.service';
// ... //
export class AppComponent {
  // ... //
  constructor(
    private exampleService: ExampleService,
  ) {}

  do42() {
    this.exampleService.addForyTwo( 7 ).subscribe( r => { console.log('42', r) });
  }

  do47() {
    this.exampleService.addForySeven( 7 ).subscribe( r => { console.log('47', r) });
  }
}

Now that we have all in place, let’s build and run our app to test all this shebang!

$ npm run build
$ npm run electron

You should see a desolate page with only two buttons and a Dev Tools open. Make sure you see the Dev Tools console and then click on each of the buttons. You should see something like: ~~~ 42 49 app.component.ts:17 47 54 app.component.ts:21 ~~~ printed out in console.

It works!

And that concludes our Angular/Electron integration voyage. There is a number of things we didn’t cover, i.e. handling on Angular side events that originate from Electron side, but those shouldn’t be too complicated to implement now that we have a working base to build upon.

The only two thing that we would like to address here are: Angular Router and app icon on MacOS, so let’s do that now.

Using Angular router in Electron

Angular Router in Electron apps? Don’t do it. Really. The Router is designed to work in Web browser environment and to solve specific issues or support specific features, such as:

  • ability to bookmark a specific UI layout/screen
  • handling of the browser’s Back button
  • providing URL to a specific UI screen for external sites/apps
  • being somewhat SEO friendly
  • etc.

All of these, and virtually all of the Router’s, functionalities are specific to that address bar in a Web browser. You don’t have address bar in Electron app (unless, for some reason, you do have one) and none of the reasons or issues that the Router was created to provide or solve.

Instead of Router, you could create a simple dedicated Angular service, injected at root level, that holds just one state variable: currently displayed screen/form/whatever. Then, use ngSwitch in your app.component.html to display whatever needs to displayed based on that UI state variable. Also, provide convenient, trivial, methods to switch current UI state, and that’s it. You could add more state variables to that service, but I would advise not to. Any state variable you think of adding is probably better suited to be placed into a dedicated Angular service that handles whatever that variable is trying to convey.

For example, you may be tempted to add a variable called currentlyOpenedDocumentID, or something like that, to that UI state service that replaces the Router. Better way would be to create a dedicated DocumentService, injected at root level, that contains all the information, state variables, including currenly opened document ID, and all of the methods and functionalities you need to work with those documents in your app. Then, use that service wherever and whenever you need to do something with your app’s document(s), including getting the ID of the currently opened document.

All that being said, resources/reload-issue.md file contains a code snippet you should add to app-electron/main.ts which will handle the issue of a blank screen when you reload Electron apps UI via ctrl+R or window.reload or, more important in our case, when electron-reload reloads the UI when it detects changes. That code snippet only handles reloads by displaying home page (index.html) when it detects that Electron failed to load the URL that Router told it to. This obviously is not a solution, it is just a temporary patch. The solution is to not use Angular Router in Electron apps, if you can at all, because you don’t need it in that environment. If you absolutely must use Router, you’ll have to figure out how to solve different issues you may have on your own, hopefully the code snippet from resources/reload-issue.md will help you to get started on that path.

That’s it about Angular Router issues in an Electron app, let’s now add an OS icon for our app for the case when it is packaged for MacOS or Windows.

App icon in MacOS and Windows

If you are going to package your application for Linux platform, the icon set in app-electron/main.ts via BrowserWindow.icon option will show up as application icon in the task bar or app dock. For that you’ll need PNG version of your application’s icon.

When it comes to Windows or MacOS, we need to specify the app icon via electron-packager options, which we set in the tools/build-package.ts script. Also, for Windows we need .ico file, and for MacOS we need .icns file.

ICO file

ICO files can be created on any of the three platforms by using ImageMagick in command line mode. You can, of course, just use the favicon.ico file that Angular created by default, but a much better thing would be to create your own from scratch because we want to support different icon sizes that also work well on desktops and task bars, not just as a browser icon.

To that end, you should create your icon in any way you like and then export/save it as a PNG file, size 1024px by 1024px. We will need it that big for MacOS ICNS file. If you don’t need ICNS file, you can downsize that to 256px by 256px.

Once you have your 1024x1024 PNG image, ICO file that contains different image sizes can be created by using ImageMagick like so, assuming your PNG file is appicon.png:

$ convert appicon.png  -background white \
          \( -clone 0 -resize 16x16 -extent 16x16 \) \
          \( -clone 0 -resize 32x32 -extent 32x32 \) \
          \( -clone 0 -resize 48x48 -extent 48x48 \) \
          \( -clone 0 -resize 64x64 -extent 64x64 \) \
          \( -clone 0 -resize 96x96 -extent 96x96 \) \
          \( -clone 0 -resize 128x128 -extent 128x128 \) \
          \( -clone 0 -resize 256x256 -extent 256x256 \) \
          -delete 0 -alpha off -colors 256 favicon.ico

This will produce favicon.ico file which you can then place into src folder from where it will automatically be picked up by tools/build-package.ts script when you run npm run build:package on Windows host.

ICNS file

For MacOS, we will need to create ICNS file, which is equivalent of ICO file from the Windows world, and for that we also need to have ImageMagick since we will also use its convert utility.

First, create your icon in any way you like and then export/save it as a PNG file, size 1024px by 1024px.

Then, run this bash script in the folder where your PNG file is:

#!/bin/bash
DEST=osx_icon.iconset
mkdir "$DEST"

convert -background none -resize '!16x16' appicon.png "$DEST/icon_16x16.png"
convert -background none -resize '!32x32' appicon.png "$DEST/icon_16x16@2x.png"
cp "$DEST/icon_16x16@2x.png" "$DEST/icon_32x32.png"
convert -background none -resize '!64x64' appicon.png "$DEST/icon_32x32@2x.png"
convert -background none -resize '!128x128' appicon.png "$DEST/icon_128x128.png"
convert -background none -resize '!256x256' appicon.png "$DEST/icon_128x128@2x.png"
cp "$DEST/icon_128x128@2x.png" "$DEST/icon_256x256.png"
convert -background none -resize '!512x512' appicon.png "$DEST/icon_256x256@2x.png"
cp "$DEST/icon_256x256@2x.png" "$DEST/icon_512x512.png"
convert -background none -resize '!1024x1024' appicon.png "$DEST/icon_512x512@2x.png"

iconutil -c icns "$DEST"
rm -R "$DEST"

Once done, you should end up with osx_icon.icns file in that same folder. Copy/move it to the project’s src folder, and the tools/build-package.ts script will pick it up when you run npm run build:package on MacOS.

And that concludes today’s presentation.

TLDR; give me all in one step

If you want to create you Angular project and then just add Electron and our simple API bridge to it, and then start coding, follow the next few steps.

First, make sure you have correct Angular CLI major version (9) for this task:

$ ng --version

...(snip)...

Angular CLI: 9.1.12
Node: 12.16.1
OS: linux x64

Angular: 
... 
Ivy Workspace: 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.901.12
@angular-devkit/core         9.1.12
@angular-devkit/schematics   9.1.12
@schematics/angular          9.1.12
@schematics/update           0.901.12
rxjs                         6.5.4

Next, create your Angular project, let’s call it your-project:

$ ng new your-project

Make sure to answer N to Angular Router (which is default) when creating the new project.

Or, add --no-routing option:

$ ng new your-project --no-routing

Next, install @ravendyne/ang-el Angular schematic package:

$ cd your-projec
$ npm i -D @ravendyne/ang-el

Use the @ravendyne/ang-el schematic to do the job for you:

$ ng add @ravendyne/ang-el
$ npm run build
$ npm run electron

That’s it.