DEV Community

Michael De Abreu
Michael De Abreu

Posted on

Integrating an Angular-CLI application with Electron - The IPC

Previously on...

In the previous sections and stories, I explained how to integrate an Angular-CLI generated application with Electron, and also how to write the same Electron application in Typescript. So far, this would allow a simple Angular application being packed as a Electron application, but there is no way for us to interact with Electron's main thread.

Introduction

In this post I'll try to explain how to get a real integrating between Angular and Electron, and be able to communicate using Electron's IPC.

What is IPC?

IPC is the inter-process communication module of Electron. With that you can send messages between your web application and the main thread application. To send and receive messages in the main thread you would have to use the ipcMain function property. Likewise, for the renderer process you would like to use the ipcRenderer.

How to use ipcMain?

Use ipcMain is a simple as require it, and use one of the functions that are able for us.



import { ipcMain } from 'electron';

ipcMain.on('ping', (event) => {
    logger('ping'); // Assume there is a logger function that would display 'ping' in console. console object does not work as-is, and it's not so easy to configure it.
}


Enter fullscreen mode Exit fullscreen mode

With that, the application will be listening a 'ping' event, and will print ping in the console. Easy peasy.

How to (normally) use ipcRenderer?

In a normal scenario, a simple require('electron').ipcRenderer would give us access to the IPC in the renderer thread. So, following the previous example, we could do something like:



const { ipcRenderer } = require('electron');

ipcRenderer.send('ping');


Enter fullscreen mode Exit fullscreen mode

This will invoke the callback function in the main thread.

But, this won't work in the Angular-CLI application. Angular-CLI underneath uses Webpack, and thus the latter will find the require key word, interpret as a call for the Node.js' require function, and will try to resolve the 'electron' module.

A Story about two requires

require as been with us for a while, since the first version of Node back in 2008, almost 10 years. But still is one of the most misunderstand functions in modern web development. With the integration of import and export keywords in Node, several articles was written to explain how the require function currently works. TL;DR: A require function is injected for every file, allowing Node to resolve the dependencies. Later, module builders will look for import and require and will try to resolve modules, assuming that's what you want.

So, now that we know that require is actually a function injected by Node, then how is require able to work in Electron renderer process. You may guessed it, Electron injects its own version of require function in the global scope of the renderer process when it loads the page. So, although it may seems like the same function, it is not.

How to use ipcRenderer in a Angular-CLI application? (Or any application bundle with Webpack)

To use ipcRenderer in our Angular-CLI app, we will leverage on the global scope. No, we won't call ipcRenderer in the global context, although we could make this works, it's not ideal. But I just told that require is a function that Electron injects in the global context. So, can we just use require? No. That's because, as I also told, Webpack will try to resolve the module requirement. There is actually another way to access a global variable, that's with the window object. The window object by default will have all the global variables, including require.

So we can just use window.require in any part of our application and it would work as expected. In this context, you cannot use window.require to require any module in your Angular-CLI application, but you can load any module that you had set in your Electron application.

Writing the service

For this example we will expose the ipc as an Angular service, and will create it using angular-cli. We follow the guide about services



ng generate service ipc -m app


Enter fullscreen mode Exit fullscreen mode

This will create our service, and update our app.module to include it in the Angular application.

Then, we write the code. We start by importing the IpcRenderer interface from electron module



import { IpcRenderer } from 'electron';



Enter fullscreen mode Exit fullscreen mode

But, we don't have any Electron module in our Angular project, how will it be resolved? We'll, actually we don't need to have the Electron module in our Angular project, because as Typescript resolver work, it will look in node_modules in folders that are children from ours project. If you want to be extra safe, or if for any reason this is not a desire behavior, you could install the @types of electron, and it won't load the hole package.



npm install @types/electron


Enter fullscreen mode Exit fullscreen mode

Next, we add a reference property inside the class to save the ipcRenderer function when we load it.



  private _ipc: IpcRenderer | undefined;


Enter fullscreen mode Exit fullscreen mode

It's important to typed it as IpcRenderer | undefined for compile the code in strict mode, as we may or may not be able to load the ipcRenderer. We now write the constructor, to assign the _ipc in load time.



  constructor() {
    if (window.require) {
      try {
        this._ipc = window.require('electron').ipcRenderer;
      } catch (e) {
        throw e;
      }
    } else {
      console.warn('Electron\'s IPC was not loaded');
    }
  }


Enter fullscreen mode Exit fullscreen mode

As you can see, we will first check if window object has a require property. With this we will assume we are inside Electron, then we will try to require('electron'), if for any reason it doesn't work it just throw an error, and the property _ipc will be undefined. Checking require in the window object will allow us to run the service in a regular browser context, in that case the _ipc won't have a assignment value and will be undefined.

You should have Typescript complaining about window not having a require property, so we need to update the project's typings file. Open /src/typings.d.ts and update with the following lines:



interface Window {
  require: NodeRequire;
}


Enter fullscreen mode Exit fullscreen mode

Now, Typescript shouldn't be annoying us.

I'll add a couple of functions to the service, just to test that it actually works as expected.



  public on(channel: string, listener: Function): void {
    if (!this._ipc) {
      return;
    }
    this._ipc.on(channel, listener);
  }

  public send(channel: string, ...args): void {
    if (!this._ipc) {
      return;
    }
    this._ipc.send(channel, ...args);
  }


Enter fullscreen mode Exit fullscreen mode

As you can see, in both we check for the _ipc property to be assigned, and then we call the functions that we want to call. We expose the same function interface of the functions we want to call, so it will be very intuitive to call them from our application.

The final service should look like:



import { Injectable } from '@angular/core';
import { IpcRenderer } from 'electron';

@Injectable()
export class IpcService {
  private _ipc: IpcRenderer | undefined = void 0;

  constructor() {
    if (window.require) {
      try {
        this._ipc = window.require('electron').ipcRenderer;
      } catch (e) {
        throw e;
      }
    } else {
      console.warn('Electron\'s IPC was not loaded');
    }
  }

  public on(channel: string, listener: IpcCallback): void {
    if (!this._ipc) {
      return;
    }
    this._ipc.on(channel, listener);
  }

  public send(channel: string, ...args): void {
    if (!this._ipc) {
      return;
    }
    this._ipc.send(channel, ...args);
  }

}


Enter fullscreen mode Exit fullscreen mode

Testing it

For testing we will call an ipc channel, and make Electron to response us back, and listen that response.

First, we will update our app.component with the following constructor function:



  constructor(private readonly _ipc: IpcService) {
    this._ipc.on('pong', (event: Electron.IpcMessageEvent) => {
      console.log('pong');
    });

    this._ipc.send('ping');
  }


Enter fullscreen mode Exit fullscreen mode

And then we will update Electron's index.ts file, importing the ipcMain module and setting a listener for the ping event, that response pong.



// First we update the import line
import { app, BrowserWindow, ipcMain, IpcMessageEvent } from 'electron';
...
ipcMain.on('ping', (event: IpcMessageEvent) => {
  event.sender.send('pong');
});


Enter fullscreen mode Exit fullscreen mode

Run the angular app with npm run electron:start and in the electron application run npm start. You should see a pong getting logged.

pong

Moving forward

There are still somethings that can be improve in the current workflow, and some of you are having troubles with native modules. But, so far we have pass for a simple Angular app, to a complete Angular/Electron integration. I'll soon uploading all this to Github, to stay tune there as well.

That's all folks

As usual, thanks you for reading this, check out my others posts. Give the post love, and share it with your friends. See you next time.

Top comments (25)

Collapse
 
8prime profile image
8-prime

I know others have said it before but you are my hero. After trying just about everything else the internet had to offer I finally got where I wanted because of this post.

Collapse
 
panyan profile image
panyann

I specially registered to thank you, without this I would spend many hours researching how to do that. 2023 and it still works! Now I can focus on what I wanted to do.

Collapse
 
matteogheza profile image
MatteoGheza • Edited

To everyone having issues with "Electron's IPC was not loaded":
You need to add contextIsolation: false to webPreferences.
Example:

webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
}
Enter fullscreen mode Exit fullscreen mode

It worked for me.

Collapse
 
jeevanavt profile image
Jeevan Antony Varghese

Thanks

Collapse
 
charlesr1971 profile image
Charles Robertson

Michael, thanks so much for the reply.

Great tutorial by the way!

I am using VSCode and I type in:

npm run electron:start

In the VSCode terminal window

Then the Chromium window opens successfully, which is good.

At this point, I get:

Electron's IPC was not loaded

In Chromium’s DEV tools console

Then I type:

npm start

In Chromium’s DEV tools console and press return.

This is when I receive the error.

Collapse
 
charlesr1971 profile image
Charles Robertson • Edited

This simply does not work.

ENVIRONMENT

Angular CLI: 8.3.25
Node: 12.14.0
OS: win32 x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.25
@angular-devkit/build-angular     0.803.25
@angular-devkit/build-optimizer   0.803.25
@angular-devkit/build-webpack     0.803.25
@angular-devkit/core              8.3.25
@angular-devkit/schematics        8.3.25
@angular/cli                      8.3.25
@ngtools/webpack                  8.3.25
@schematics/angular               8.3.25
@schematics/update                0.803.25
rxjs                              6.4.0
typescript                        3.5.3
webpack                           4.39.2

I have copied everything precisely and after the window opens successfully, I type in:

npm start 

Into Chromium's Dev Tool input.
I get the following error:

VM140:1 Uncaught SyntaxError: Unexpected identifier

I also get a warning in the console:

Electron's IPC was not loaded
Collapse
 
michaeljota profile image
Michael De Abreu

You have to run npm run electron:start in the Angular app, and npm start in the electron app.
That being said, I have to update this as this may be outdated.

Collapse
 
popoever profile image
Andre Pan

Seems I'm not the only one keep getting 'IPC was not loaded', which means window.require is still not allowed? Tried almost every solution on the web, yours is the closest one to my own, but unfortunately, both mine and yours don't work, the only difference is mine is based on a pretty old version of electron and angular, I just updated them to the latest version, but the entry point remains main.js instead of index.ts, well, no idea what's going wrong

Collapse
 
joohansson profile image
joohansson

I'm stuck with the exact same issue in another angular app using electrum. I think the code was taken from this guide because it's pretty much identical but was broken when upgrading electrum and angular. The window object is undefined and I've tried adding the nodeIntegration: true in webPreferences but no difference.

It doesn't get passed this step (which worked perfectly fine before): github.com/BitDesert/Nault/blob/c3...

Still no solution to this?

Collapse
 
jessyco profile image
Jessy

Missing from this article is that, for window.require to work you also need to make sure your BrowserWindow you create has the option

...
webPreferences: {
  nodeIntegration: true,
}
Collapse
 
michaeljota profile image
Michael De Abreu

Thanks! Looks like one of the changes they introduced with Electron 4. electronjs.org/blog/electron-4-0

Collapse
 
charlesr1971 profile image
Charles Robertson

Yes. I have added:

webPreferences: {
  nodeIntegration: true,
}

But, I still cannot get the IPC communication working.

Collapse
 
adammauger profile image
AJM

What's IpcCallback? Googling for it...

Collapse
 
jessyco profile image
Jessy

you can simply replace that with any to get things going :)