2

I did my first try and wanted to use the electron apis from an angular renderer process. I followed the instructions in

Instruction creating Angular -Electron application

so in my main.js file I added:

const {app, BrowserWindow, ipcMain} = require('electron')

and I also added

function openModal(){
  const { BrowserWindow } = require('electron');
  let modal = new BrowserWindow({ parent: mainWindow, modal: true, show: false })
  modal.loadURL('https://www.sitepoint.com')
  modal.once('ready-to-show', () => {
    modal.show()
  })
}

ipcMain.on('openModal', (event, arg) => {
  openModal()
})

In my app.component.ts file a added the import import { IpcRenderer } from 'electron';

and I added the following constructor

  private ipc: IpcRenderer
  constructor(){
    if ((<any>window).require) {
      try {
        this.ipc = (<any>window).require('electron').ipcRenderer;
      } catch (e) {
        throw e;
      }
    } else {
      console.warn('App not running inside Electron!');
    }
  }

Since it is not totally clear to my CLI that icp will be of type IpcRenderer I added in this line

private ipc: IpcRenderer | any;

With the function

  openModal(){
    console.log("Open a modal");
    this.ipc.send("openModal");
  }

it should be able to send a something to the "main" process. But if I call the function I get the error

TypeError: Cannot read property 'send' of undefined

What do I made wrong?

nodefastic
  • 21
  • 2

1 Answers1

3

I had also been facing the same issue but found a long hack.

The cause

Take a look at this line:

(<any>window).require

This line searches for the require() method in the global window object, which is a NodeJS function and hence won't be injected during runtime if you're using Electron version > 5, because the webPreferences -> nodeIntegration property of the BrowserWindow class was set to false by default.

The solution

There are two solutions:

  1. set nodeIntegration: true and contextIsolation: false The app will work as expected BUT there is a security issue with this hack. As quoted in this answer:

Electron apps are great because we get to use node, but this power is a double-edged sword. If we are not careful, we give someone access to node through our app, and with node a bad actor can corrupt your machine or delete your operating system files (among other things, I imagine).

  1. Using contextBridge and preload.js: This is derived from the above answer and other answers to this question.

I'll be presenting a workaround using Angular.

in the webPreferences when creating the mainWindow in main.js, make the following modifications:

webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      enableRemoteModule: false,
      preload: path.join(__dirname, "preload.js")
    }

In the same directory where main.js is there, create a preload.js file and add the following code there (you may omit the receive() declaration as for this use case it is not required).

const {
    contextBridge,
    ipcRenderer
} = require("electron");

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        send: (channel, data) => {
            // whitelist channels
            let validChannels = ["openModal"];
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, data);
            }
        },
        receive: (channel, func) => {
            let validChannels = ["fromMain"];
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => func(...args));
            }
        }
    }
);

PS: Reason for adding this code is already mentioned in the referenced answer so I won't be repeating that here.

Now, at runtime, electron will inject window.api.send() and window.api.receive() methods in the global window object. If you try to access them directly in your angular component, linter will give error because they are not defined on the standard window object. To solve this, we will create an Angular service and inject in our application. This service will reference to the window object of the browser created at runtime, with the above two functions added.

Create a WindowRefService and add the following code:

window-ref-service.ts

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

@Injectable({
  providedIn: 'root'
})

export class WindowRefService {

  getWindow(): any {
    return window;
  }
  constructor() { }
  get nativeWindow(): Window{
    return this.getWindow();
  }
}

Don't forget to register the service in the AppModule:

...

import { WindowRefService } from './window-ref.service';
...
@NgModule{
...
providers: [WindowRefService]
}

After that inject it into your component:

 import { WindowRefService } from './window-ref.service';
 ...
 
 private _window:any;
 constructor(windowRef: WindowRefService){
     this._window = windowRef.nativeWindow;
 }
 ...
 //openModal function
 openModal(){
  console.log("Open a modal");
  this._window.api.send("openModal", /* data to be sent, if any*/);
 }
 

Remove the private ipc: IpcRenderer | any; and other existing code in the constructor as it is not required.

Hardik
  • 31
  • 5