Ang-El: Angular-Electron howto app
Electron 11 with Angular 9 and Node 12, from scratch
- Create initial Angular project
- Create initial Electron app
- Initial versions of build scripts and first app run
- Live reload for developers
- Use Angular
server
builder to build Electron app - Packaging the project
- Communication between Angular UI and Electron backend
- Using Angular router in Electron
- App icon in MacOS and Windows
- TLDR; give me all in one step
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.