I'm running an Angular Frontend within a frameless Electron-Window. Because the window is frameless, I need to implement the minimizing/maximizing/unmaximizing/closing-behaviour myself. I have a button for maximize and one for unmaximize and would like to hide one of them at all times, depending on the window state.
I have node-integration set to false and wonder how I can communicate from Electron to my Angular-Frontend. Then I would only need to find a way to get my app-window and emit an event, whenever it is maximized/unmaximized and then change my UI accordingly.
My communication from Angular to Electron works like this:
in Angular I have an 'electronService' which is injected in my components and calls the electron functions.
In my preload.js I expose a function from my main.js to my renderer-process like this:
const { ipcRenderer, contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
maximizeWindow: () => {
return ipcRenderer.invoke('electron::maximize-window');
}
});
And in my main.js I handle the incoming calls like this:
app.whenReady().then(() => {
ipcMain.handle('electron::maximize-window', maximizeWindow);
});
function maximizeWindow(_) {
...
}
Is there a way to do this in the opposite direction?
Though I don't use Angular, the implementation of your maximize / restore functionality is quite simple once laid out, understood and implemented correctly. The use of IPC and the detection and communication of your window state will be core to its functionality.
Desired functionality:
When the window is maximised, show the restore button.
When the window is not maximised (restored), show the maximise button.
Implementation:
Our render will show all four buttons: Minimise, restore, maximise and close.
On render button click, send an IPC message to the main process to implement window functionality.
Depending on the window state (change) in the main process, send a message to the render process to show / hide respective buttons.
On window creation, send a message to the render process to show / hide respective buttons.
Detect when the title bar is double-clicked to toggle render buttons between maximise and restore.
You may wonder why point 3 is needed? It exists due to points 4 and 5. Point 4 is used on window creation. No matter what settings you use to create your window (manually or pulled from a .json file), your newly created window will dynamically display the correct button.
Point 5 is when you double-click your title bar in and out of maximise / restore.
Within your preload.js script we need two functions. One to indicate which render button was click (point 2) and the other to receive the state of the window (point 3).
preload.js (main process)
const { ipcRenderer, contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
buttonClicked: (button) => {
ipcRenderer.send('buttonClicked', button);
},
windowState: (state) => {
ipcRenderer.on('windowState', state);
}
});
Within your main.js script we add functionality to tell the render process what state the window is in upon creation (point 4).
Here, we also receive the button click message from the render process (point 2) and implement functionality accordingly.
Lastly, listen for double-clicking of the title bar (point 5).
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
let window;
function initialiseButtons() {
if (window.isMaximized()) {
window.webContents.send('windowState','maximised')
} else {
window.webContents.send('windowState','restored')
}
}
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
frame: false,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
// .then(() => { window.maximize(); }) // Testing
.then(() => { initialiseButtons(); })
.then(() => { window.show(); });
// Double-click of title bar from restore to maximise
window.on('maximize', () => {
initialiseButtons();
})
// Double-click of title bar from maximise to restore
window.on('unmaximize', () => {
initialiseButtons();
})
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
electronIpcMain.on('buttonClicked', (event, buttonClicked) => {
if (buttonClicked === 'minimise') {
window.minimize();
return;
}
if (buttonClicked === 'restore') {
window.restore();
window.webContents.send('windowState','restored');
return;
}
if (buttonClicked === 'maximise') {
window.maximize();
window.webContents.send('windowState','maximised');
return;
}
if (buttonClicked === 'close') {
window.close();
}
})
Lastly, we send the button click event(s) to the main process (point 1) and listen for the window state upon window creation (point 4).
For simplicity, i have overlooked the use of inline styling and buttons for interactivity.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body style="margin: 0; padding: 0;">
<div style="display: flex; flex-flow: row nowrap; padding: 5px; background-color: #ccc;">
<div style="flex: 1 0 auto">Title</div>
<div style="flex: 0 1 auto">
<input type="button" id="minimise" value="_">
<input type="button" id="restore" value="❐">
<input type="button" id="maximise" value="☐">
<input type="button" id="close" value="☓">
</div>
</div>
</body>
<script>
let restoreButton = document.getElementById('restore');
let maximiseButton = document.getElementById('maximise');
document.getElementById('minimise').addEventListener('click', () => {
window.electron.buttonClicked('minimise');
});
document.getElementById('close').addEventListener('click', () => {
window.electron.buttonClicked('close');
});
restoreButton.addEventListener('click', () => {
window.electron.buttonClicked('restore');
});
maximiseButton.addEventListener('click', () => {
window.electron.buttonClicked('maximise');
});
window.electron.windowState((event, state) => {
if (state === 'maximised') {
restoreButton.style.display = 'inline-block';
maximiseButton.style.display = 'none';
} else {
restoreButton.style.display = 'none';
maximiseButton.style.display = 'inline-block';
}
})
</script>
</html>
Related
I have scoured the internet and found no answers to this on Stack or anywhere for that matter that actually work.
I developed a SPA (PWA). Within the SPA the user can click a button to open a new page that contains a pricelist. This is easy enough. However, from the new pricelist page I want to be able to call a function from the SPA and pass arguments to that function. The function is a module that is imported to the original SPA.
Essentially, I want the pricelist page to be able to pass the partnumber from the pricelist page to the shopping cart in the SPA.
Is this even possible? If so, can you share example or link?
I wrote the following code to handle the communications between the main application and the pricelist page. It works flawlessly. I had to do a bit of testing to figure it all out. I am using the BROADCAST CHANNEL API and the SESSIONID.
// PWA MAIN APPLICATION (index.js)
const BROADCAST_CHANNEL_01 = new BroadcastChannel(sessionStorage.getItem('SESSIONID'))
BROADCAST_CHANNEL_01.onmessage = (event) => {
if (event.data.partnumber) {
ReturnSelectedItem(event.data.partnumber, event.data.model)
BROADCAST_CHANNEL_01.postMessage({
partnumber: event.data.partnumber,
model: event.data.model
})
}
if (event.data === 'RE-OPEN-CHANNEL') {
//// RE-OPENS CHANNEL
BROADCAST_CHANNEL_01.postMessage(`${'CHANNEL_OPENED'}`)
}
if (event.data === 'beforeunload') {
//// CHANNEL WILL RE-OPEN IF PAGE WAS ONLY REFRESHED AND NOT CLOSED
setTimeout(() => {
BROADCAST_CHANNEL_01.postMessage(`${'CHANNEL_OPENED'}`)
}, 1000)
}
}
//// LET PRICELIST PAGE "KNOW" IT CAN NO LONGER ADD TO MAIN APP CART
window.addEventListener('beforeunload', function () {
BROADCAST_CHANNEL_01.postMessage(`${'beforeunload'}`)
})
//PRICELIST.js
const BROADCAST_CHANNEL_01 = new BroadcastChannel(
sessionStorage.getItem('SESSIONID')
)
BROADCAST_CHANNEL_01.onmessage = (event) => {
if (event.data === 'CHANNEL_OPENED') {
sessionStorage.setItem('BROADCAST_CHANNEL', 'OPENED')
}
if (event.data === 'beforeunload') {
sessionStorage.setItem('BROADCAST_CHANNEL', 'CLOSED')
//// CHANNEL WILL RE-OPEN IF PAGE WAS ONLY REFRESHED AND NOT CLOSED
setTimeout(() => {
BROADCAST_CHANNEL_01.postMessage(`${'RE-OPEN-CHANNEL'}`)
}, 1000)
}
}
window.addEventListener('beforeunload', function () {
BROADCAST_CHANNEL_01.postMessage(`${'beforeunload'}`)
})
//PRICELIST.js Event Listener to pass in the partnumber/model to the main application
if (event.target.matches('.quick-add-btn')) {
if (!event.target.classList.contains('added')) {
const PART_NUMBER =
event.target.parentNode.parentNode.childNodes[0].innerText
const MODEL = event.target.parentNode.parentNode.childNodes[2].innerText
if (sessionStorage.getItem('BROADCAST_CHANNEL') === 'OPENED') {
BROADCAST_CHANNEL_01.postMessage({
partnumber: PART_NUMBER,
model: MODEL
})
event.target.innerHTML = '✓' //html check mark
event.target.classList.remove('quick-add-btn')
}
if (
sessionStorage.getItem('BROADCAST_CHANNEL') === 'CLOSED' ||
!sessionStorage.getItem('BROADCAST_CHANNEL')
) {
CallModal(
'To use this feature...\r\nOpen the pricelist from the main application...\r\nUse the "Pricelist" button',
'alert'
)
}
}
return
}
On a classic front-end JavaScript, capturing the "window close" event can be done in a multiple ways. It can be done with:
// Method A: `close` event via an event listener
window.addEventListener( 'close', function(event){ /** your magic here **/ } )
// Method B: `beforeunload` event via an event listener
window.addEventListener( 'beforeunload', function(event){ /** your magic here **/ } )
// Method C: classic `onclose` event binder
window.onclose = function(event){ /** your magic here **/ }
// Method D: classic `onbeforeunload` event binder
window.onbeforeunload = function(event){ /** your magic here **/ }
I tried these methods inside the Electron's renderer script/environment but the window close event doesn't seem to get triggered.
Which brings to my question, how to capture the "window close" event on Electron's renderer process?
According to Electron's official documentation:
Event: "before-quit" Emitted before the application starts closing its windows. Calling event.preventDefault() will prevent the default behavior, which is terminating the application.
So you need to use the before-quit event on your Electron application.
Normally, proving that these events trigger (or not) is difficult as the render process would normally close prior to confirmation.
Instead, through the use of Inter-Process Communication, one can tell if any of these events trigger by sending one or more messages back to the main process for viewing.
To test this, I have implemented a simple preload.js, main.js and index.html script.
preload.js (main process)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld(
'ipcRender', {
send: (message) => {
ipcRenderer.send('closing', message);
}
});
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Testing
electronIpcMain.on('closing', (event, message) => {
console.log(message);
})
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
</head>
<body>
<input type="button" id="button" value="Close">
</body>
<script>
// Method A: `close` event via an event listener
window.addEventListener('close', (event) => {
window.ipcRender.send('Method A'); // Doesn't work
})
// Method B: `beforeunload` event via an event listener
window.addEventListener('beforeunload', (event) => {
window.ipcRender.send('Method B'); // Works
})
// Method C: classic `onclose` event binder
window.onclose = (event) => {
window.ipcRender.send('Method C'); // Doesn't work
}
// Method D: classic `onbeforeunload` event binder
window.onbeforeunload = (event) => {
window.ipcRender.send('Method D'); // Works
}
// Method E: `click' event via an event listener
document.getElementById('button').addEventListener('click', () => {
// Let's pretent that this can close the window from the main process
window.ipcRender.send('Method E');
})
</script>
</html>
Below is the result when closed via the traffic light close button or the task bar close menu.
Method B // `beforeunload` event via an event listener
Method D // classic `onbeforeunload` event binder
Depending on how one (correctly or incorrectly) implements their preload.js script, one may receive no results compared to the results shown above.
Whilst I usually implement my preload.js script differently to that shown above, I have kept it simple and inline with what most people appear to be familiar with. IE: Context Isolation (Enabled)
I have created a button(Connect), when I click it should go to the other page(second.html). It works with localhost, but not with Electron App. What am I doing wrong?
<script>
$(document).ready(() => {
$( '#buttonConnect').on('click',() =>{
const inputIp = $('#ip');
if(inputIp.val() !== ""){
window.location.href = "observer.html ?ip="+inputIp.val();
}else{
alert("Check Connection Settings!");
}
});
});
</script>
<button class="btn btn-primary" id="buttonConnect">Connect</button>
You need to communicate with electron from your remote window to tell it which URL to load.
Using IPC is the recommended way of doing that.
(a former solution was using remote which is deprecated now)
In your remote code (your jQuery code from above, in this case):
let { ipcRenderer } = require("electron");
$(document).ready(() => {
$( '#buttonConnect').on('click',() =>{
const inputIp = $('#ip');
if (inputIp.val() !== ""){
ipcRenderer.send("load-page", `second.html?ip=${inputIp.val()}`);
}
else {
alert("Check Connection Settings!");
}
});
});
In your main electron code (probably app.js):
let electron = require("electron");
let { BrowserWindow, ipcMain } = electron;
// ...
ipcMain.on("load-page", (event, uri) => {
let win = new BrowserWindow({ /* ... */ });
// I don't know your directory structure.
// You may have to adapt the following line.
win.loadURL(`file://${__dirname}/${uri}`);
});
What I'm trying to achieve is "real" app-like behaviour in a sense that when I close the app on MacOS (hit X, so the app is still in dock) and afterwards open the app from dock again, the webpage content should be there immediately. As I'm trying to build a container for a web-app, the behaviour I'm getting is that every time I open the app, the web page is loaded again causing friction in the UX.
I've tried some dirty workarounds, like calling .hide() on main window from the renderer process before unloading the window:
const {remote} = require('electron');
const main = remote.require('./main.js');
window.onbeforeunload = e => {
main.hideWindow();
e.returnValue = false;
};
and in main process
exports.hideWindow = () => {
mainWindow.hide();
};
But that way I cannot quit my app at all.
Another option I considered was to load the whole mainWindow DOM in the memory, then upon opening the app, in the <webview> preload script load the cached content into the webview and once the page loads, overwrite the webview content, but it also seems very hackish.
I know Slack behaves exactly how I want my app to behave, but I'm struggling to find how they achieve that instant-load (or perhaps not ever closing, except when Quit is selected from the Dock or Cmd+Q is hit).
If I understand your issue correctly then I think there are a couple of standard workarounds for this. Specifically around
...not ever closing, except when Quit is selected from the Dock or
Cmd+Q is hit)
// 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()
}
})
And...
open the app from dock again, the webpage content should be there
immediately.
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 (mainWindow === null) {
createWindow()
}
})
I ended up having a flag which governs the "closing" behaviour
let allowQuitting = false;
and handling the closing like this
function quitApp() {
if (app) {
allowQuitting = true;
app.quit();
}
}
function closeApp() {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
}
}
On closing, I listen on closing events
function createWindow() {
mainWindow.on('closed', function () {
closeApp();
});
mainWindow.on('close', event => {
if (allowQuitting === false) {
event.preventDefault();
closeApp();
}
});
...
}
app.on('ready', createWindow);
On activating, I first check if the window exists
app.on('activate', function () {
if (mainWindow === null) {
createWindow();
} else {
mainWindow.show();
}
});
The app can be closed using Cmd+Q due to the accelerator:
const template = [{
label: 'Application',
submenu: [
...
{
label: 'Quit', accelerator: 'Command+Q', click: () => quitApp()
}
]
},
...
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
This gives me the desired result, albeit with a side-effect. The app can be closed with Cmd+Q, but can't be closed from the dock (by selecting Quit) or when shutting the system down (says it interrupted the shutdown).
I am writing an application in electron where if a user has a unsaved file open I want to prompt the user before saving it. I found this example code online:
window.onbeforeunload = (e) => {
var answer = confirm('Do you really want to close the application?');
e.returnValue = answer; // this will *prevent* the closing no matter what value is passed
if(answer) { mainWindow.destroy(); } // this will close the app
};
This code strangely works if the dialogs Yes, Cancel or X button is pressed within a few seconds of appearing but if you let the dialog rest on screen for a little and then click a button the application will close no matter what is pressed.
This code is located in the my main script file called by index.html
Really strange behavior! I cannot explain why it's happening, but can give you a workaround implemented in main process.
You can use electron's dialog module and create the same confirmation dialog with electron. This one works as expected.
main.js
const { app, BrowserWindow, dialog } = require('electron')
const path = require('path')
app.once('ready', () => {
let win = new BrowserWindow()
win.loadURL(path.resolve(__dirname, 'index.html'))
win.on('close', e => {
let choice = dialog.showMessageBox(
win,
{
type: 'question',
buttons: ['Yes', 'No'],
title: 'Confirm',
message: 'Do you really want to close the application?'
}
)
if (choice === 1) e.preventDefault()
})
})
It might be possible only when DevTools window is activated.
In any case, prefer working with the event close as pergy suggested above. This is so far the best approach.
But be aware that e.preventDefault() is spreading everywhere in the code. Once you managed properly the preventDefault() you need to turn the variable e.defaultPrevented = false to get back to the natural behavior of your app.
Actually, it seems e.preventDefault() function is turnind the variable e.defaultPrevented to true until you change its value.
In my case, I had to use a variable called modificationEnCours which is true when I don't want to close my window and then false if I want to like that:
let mainWindow
let mainMenu // Menu de la fenêtre principale
app.on('ready', () => {
// Listen for app to be ready
// Create the mainWindow
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
minHeight: 350,
minWidth: 500,
frame: true,
webPreferences: {
nodeIntegration: true
}
})
// Quit app when window is closed
mainWindow.on('close', function(e){
console.log('close')
if (modificationEnCours){
e.preventDefault()
if(msgBoxVerifieSauvegarde('Question','Voulez-vous enregistrer avant de quitter ?')) {
modificationEnCours=false
app.quit()
}
} else if (process.platform !== 'darwin') {
modificationEnCours=false
app.quit()
mainWindow = null
}
})
// Load html in window
mainWindow.loadFile(path.join(__dirname, 'mainWindow.html'))
// Build menu from template
mainMenu = Menu.buildFromTemplate(mainMenuTemplate)
// Insert menu
Menu.setApplicationMenu(mainMenu)
})