4

I'm trying to figure out how I can close an electron app with an angular component. I removed the menu bar by setting frame: false on BrowserWindow({... inside main.js. I have a close button on the top right corner of the electron app from a component. I want to be able to close the app from the component.ts file on click, but I haven't seen any examples of closing electron from an angular component.

I thought the following would work, but didn't. I'm exporting a function from main.js and calling the function from the component. like so (see closeApp()):

const { app, BrowserWindow } = require('electron')
const url = require("url");
const path = require("path");

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    frame: false,
    webPreferences: {
      nodeIntegration: true
    }
  })
  mainWindow.maximize();
  mainWindow.loadURL(
    url.format({
      pathname: path.join(__dirname, `/dist/index.html`),
      protocol: "file:",
      slashes: true
    })
  );

...

function closeApp() {
  mainWindow = null
}

export { closeApp };

Then I would try and import it into component like

import { Component, OnInit } from '@angular/core';
import { closeApp } from '../../../main.js';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {

  }
  testClose() {
    closeApp();
  }

}

How can I close the electron app from angular? I appreciate any help!

user6680
  • 79
  • 6
  • 34
  • 78
  • `I get build errors with electron` ... Can you share those errors? – Roddy of the Frozen Peas Feb 08 '21 at 23:06
  • Correction: I did have build errors, but they were unrelated. The build errors are gone now so that's not the issue. – user6680 Feb 08 '21 at 23:11
  • I think the answers from this question still apply: https://stackoverflow.com/questions/43314039/how-to-close-electron-app-via-javascript – Roddy of the Frozen Peas Feb 08 '21 at 23:20
  • If I try this solution: https://stackoverflow.com/a/43314199/4350389 then I get compiler errors. Here are my updated changes and compiler errors from that solution. https://pastebin.com/yXLqaKXz Do you know how to resolve these errors? – user6680 Feb 08 '21 at 23:54
  • Can't you use IPC calls? I usually have an Angular service that does all of the communication with the Node process. You could look at [this article](https://dev.to/michaeljota/integrating-an-angular-cli-application-with-electron---the-ipc-4m18) for more explanation – RJM Feb 09 '21 at 11:42
  • I followed that article and I created the service, but I can't get it setup right in main.ts without it throwing some error. For instance if I ```import { ipcMain, IpcMessageEvent } from 'electron';``` inside main ts it will say ```Cannot use import statement outside a module``` but that's how the article has it setup. I tried importing it with require, but that didn't work either. I created a simplified repo of the code so maybe you can see what the issue is... https://github.com/user6680/angular-ipc . I appreciate the help! – user6680 Feb 10 '21 at 23:41

3 Answers3

1

You can use code like this from your browser window. Keep in mind it requires enableRemoteModule which is not recommended for security reasons. You can work around that with more complex code, but this should show you the basic idea

The easier but less secure way

In your main process:

mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    frame: false,
    fullscreen: false,
    webPreferences: {
      nodeIntegration: true,
      enableRemoteModule: true
    }
  })

From your mainWindow, in the renderer

Upon clicking the close button for your app (your custom button) you should have it run the following logic:

const win = remote.getCurrentWindow();
win.close();

https://www.electronjs.org/docs/api/browser-window#winclose

win.destroy() is also available, but win.close() should be preferred.


The harder but more secure way

This requires using a preload script That's another question, but I'll include the code for doing it this way, but it is harder to get working for a number of reasons.

That said, this is the way you should aim to implement things in the long run.

  import { browserWindow, ipcMain } from 'electron';

  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    frame: false,
    fullscreen: false,
    preload: [Absolute file path to your preload file],
    webPreferences: {}
  });

  ipcMain.handle('close-main-window', async (evt) => {
    mainWindow.close();
    // You can also return values from here

  });


preload.js

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

  // We only pass closeWindow to be used in the renderer page, rather than passing the entire ipcRenderer, which is a security issue
  const closeWindow = async () => {
     await ipcRenderer.invoke('close-main-window'); // We don't actually need async/await here, but if you were returning a value from `ipcRenderer.invoke` then you would need async/await
  }

  window.api = {
    closeWindow
  }

renderer page

  
// I don't know Angular, but just call window.api.closeWindow() in response to the button you want to use

Slbox
  • 10,957
  • 15
  • 54
  • 106
  • Am I supposed to declare remote like ```const remote = require('electron').remote;``` in order to use I add this line at the top of my angular component, I get a bunch of errors: https://postimg.cc/fSbG0t9D. Maybe I'm defining ```remote``` wrong Also you said there's a better way that is more secure. I'm building something that I would hope be a full fledged product one day so security is a priority. I can't work around it if my question is how to do it lol. Can you provide more details on a more secure way to closing the app from the component? – user6680 Feb 14 '21 at 23:25
  • You'll also need to enable `nodeIntegration` when creating your window. See https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions -- As you mention, you can't work around it if your question is "how to." -- The alternative way to do this more securely would be to use `ipcRenderer.invoke` and `ipcMain.handle` but it's quite complex compared to the code above. I'd recommend using the code above, and once you understand it you can implement it using IPC instead of `remote`. – Slbox Feb 14 '21 at 23:41
  • I tried your code as follows in my component: https://pastebin.com/Xi7vrH64 but I get error: https://postimg.cc/rzHNVJKr . Here's the code I'm using in the component.ts file: https://pastebin.com/Xi7vrH64 To resolve this, I've googled around and it seems I can add ```"types": ["node"]``` to either tsconfig.json or tsconfig.app.json or both and run npm install @types/node, but then I get ```Module not found: Error: Can't resolve 'fs' in 'C:\Users\Phil\Documents\...``` – user6680 Feb 16 '21 at 00:37
  • I tried the changes also in my skeleton version of my code here: https://github.com/user6680/angular-ipc, but same issue. Feel free to take a stab at it with the repo code and let me know what you think. – user6680 Feb 16 '21 at 00:37
  • https://github.com/user6680/angular-ipc/blob/ecaddf3c446b9acacb411473c8f0a11ac57ed7b0/main.ts#L17 Remove `contextIsolation` and replace it with `nodeIntegration: true` Once you get that working (or don't if I overlooked something), let me know. You should do some reading about those settings to understand what they do. `contextIsolation` is quite restrictive. – Slbox Feb 16 '21 at 02:19
  • In the main.ts file webPreferences is set up like ```webPreferences: { nodeIntegration: true }``` now, but in the component your solution uses remote which I've been trying to declare unsuccessfully. If I declare remote like ```const remote = require('electron').remote;``` then I get https://postimg.cc/xk7QzhwX. How can I declare remote so that i can use it in the component so that I can execute the code in your answer? I'll release the bounty since it's expiring and you've been the most helpful, but I hope you can still help me get passed this issue. – user6680 Feb 16 '21 at 23:54
  • @user6680 it looks like you still have `contextIsolation` enabled and `nodeIntegration` disabled on that line. I'll make some edits to my answer to try to clarify. – Slbox Feb 17 '21 at 00:15
  • @user6680 take a look now, let me know if that helps. – Slbox Feb 17 '21 at 00:24
  • I was making the changes locally, but wasn't commiting them. That's my bad. I created a branch just for you with the latest: https://github.com/user6680/angular-ipc/tree/latest_changes – user6680 Feb 17 '21 at 01:40
  • You'll still need to set `enableRemoteModule: true` https://www.electronjs.org/docs/breaking-changes#default-changed-enableremotemodule-defaults-to-false What is the outcome after you enable that? – Slbox Feb 17 '21 at 19:54
  • 1
    I changed my code to to add ```enableRemoteModule: true``` , but it still throws that error when I uncomment out ```const remote = require('electron').remote;``` in the angular component. References: https://postimg.cc/rR0njm3S and https://postimg.cc/rKGCkx1D. If I comment out that line ```const remote = require('electron').remote;``` and ```const win = remote.getCurrentWindow(); win.close();``` then the error doesn't happen – user6680 Feb 17 '21 at 23:26
  • Unfortunately I'm not clear on what those errors may be caused by. I'm not too familiar with TypeScript, It seems like possibly an issue in your build process/configuration. You can try using instead `import { remote } from 'electron'` - but if that doesn't work I think you'll probably need to post a new question for this new issue. I'd like to be of more help but I think we've hit a wall with what I can help with here. That said, feel free to ping me with updates and questions. – Slbox Feb 18 '21 at 22:04
  • Specifically look into `"Field 'browser' doesn't contain a valid alias configuration"` - that indicates a likely problem with the build process or configuration, unless I'm misinterpreting something. – Slbox Feb 18 '21 at 22:05
  • @user6680 did this answer work for you in the end? If so, it would make my day if you'd accept the answer! – Slbox Oct 28 '21 at 19:22
0

Add this code in your main.ts file

mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store window
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    
    mainWindow = null;
    
  });

return mainWindow;

mainWindow.on("Closed") will capture the click event on cross icon and that time we will return the win null which will close the window.

My main.ts file and it's function.

import { app, BrowserWindow, screen, Menu } from 'electron';
import * as path from 'path';
import * as url from 'url';
import * as electronLocalshortcut from 'electron-localshortcut'

let win: BrowserWindow = null;
const args = process.argv.slice(1),
  serve = args.some(val => val === '--serve');

function createWindow(): BrowserWindow {

  const electronScreen = screen;
  const size = electronScreen.getPrimaryDisplay().workAreaSize;

  // Create the browser window.
  win = new BrowserWindow({
    x: 0,
    y: 0,
    width: size.width,
    height: size.height,
    icon: path.join(__dirname, 'dist/assets/logo.png'),
    webPreferences: {
      nodeIntegration: true,
      allowRunningInsecureContent: (serve) ? true : false,
    },
  });


  // Disable refresh
  win.on('focus', (event) => {
    console.log("event of on fucous ");
    electronLocalshortcut.register(win, ['CommandOrControl+R', 'CommandOrControl+Shift+R', 'F5'], () => { })
  })

  win.on('blur', (event) => {
    console.log("event of on blue ");
    electronLocalshortcut.unregisterAll(win)
  })
  if (serve) {
    require('electron-reload')(__dirname, {
      electron: require(`${__dirname}/node_modules/electron`)
    });
    win.loadURL('http://localhost:4200');
  } else {
    win.loadURL(url.format({
      pathname: path.join(__dirname, 'dist/index.html'),
      protocol: 'file:',
      slashes: true
    }));
  }

  if (serve) {
    win.webContents.openDevTools();
  }

  win.on('close', (e) => {
    // Do your control here

    e.preventDefault();

  });
  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store window
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    // if (JSON.parse(localStorage.getItem('isRunning'))) {
    //   alert('Your timer is running')
    // } else {
    win = null;
    // }
  });


  return win;
}

try {

  // Custom menu.
  const isMac = process.platform === 'darwin'

  const template: any = [
    // { role: 'fileMenu' }
    {
      label: 'File',
      submenu: [
        isMac ? { role: 'close' } : { role: 'quit' }
      ]
    },
    // { role: 'editMenu' }
    {
      label: 'Window',
      submenu: [
        { role: 'minimize' },
      ]
    }
  ]

  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)

  // This method will be called when Electron has finished
  // initialization and is ready to create browser windows.
  // Some APIs can only be used after this event occurs.
  app.on('ready', createWindow);

  // Quit when all windows are closed.
  app.on('window-all-closed', () => {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
      app.quit();
    }
  });

  app.on('activate', () => {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
      createWindow();
    }
  });

} catch (e) {
  // Catch Error
  // throw e;
}

Thank you.

Pushprajsinh Chudasama
  • 7,772
  • 4
  • 20
  • 43
  • How are you declaring ```win```? If you look at my main.ts https://github.com/user6680/angular-ipc/blob/master/main.ts you'll see it hasn't been defined so I'm not sure how you're defining it. Currently ```mainWindow``` is the window object I believe, which is why I was setting it to null. Also I'm using ipc to try and send the close command from an angular component so ```frame``` is set to false inside that ```BrowserWindow({...``` object so the button being clicked is from angular and not the frame. – user6680 Feb 12 '21 at 13:53
  • Replace win with mainWindow. My mistake. @user6680 – Pushprajsinh Chudasama Feb 15 '21 at 07:50
  • 1
    Here is how my main.ts file looks like. I am updating my answer. @user6680 – Pushprajsinh Chudasama Feb 15 '21 at 07:51
  • 1
    I appreciate the update, but two things. I updated my main.ts file to use your edited main.ts file, but I get https://postimg.cc/wyX24Cwb. Also, it doesn't answer my question on how to close the electron app from the angular component. If this ran without that error, I believe it would only close the app via the electron frame and not from an angular component. Preferably I'd like to see how it's done via IPC angular service per my original question. Here is a sample repo I created to demonstrate what I'm trying to do via IPC from Angular service https://github.com/user6680/angular-ipc – user6680 Feb 15 '21 at 17:13
  • @user6680 have you tried my answer? It would show you how to solve your issue if you would give it a try. The only difference in implementing it via `IPC` is that you have to write more code to do the same thing. If you understand the simple code I shared, then you will be 95% of the way to solving it with `IPC` as well. – Slbox Feb 15 '21 at 21:03
0

This may or may not be the greatest approach, but it appears to be a workaround: To capture this event, I used 'window.onbeforeunload'.

Ex:

export class AppComponent implements OnInit {

  
  ngOnInit(): void {   

    window.onbeforeunload = (e: BeforeUnloadEvent) => {
     // Do something...
    }
  }
}
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 15 '21 at 14:39