Electron: Wait for a message from renderer process - javascript

Scenario:
I have an Electron app. I also have a main process and a renderer process (which is a React component). There are a number of questions available for the main process. The main process sends these questions to the renderer process one question at a time. Once the renderer process receives a question, it displays it to the user and the user needs to answer that question and hit submit. Once, submit is hit, the answer is sent back to the main process for further processing after which the main process sends the next question and so on until all the questions are answered.
Current state:
I am able to send the question to the renderer, but I don't know how to wait for the answer.
Code to illustrate the idea:
Main:
// Get all questions
const questions = [...];
// Iterate through questions
for (const question of questions) {
// Send question
window.getAllWindows()[0].webContents.send('question', { question: question });
// TODO: Wait for answer
// TODO: do something with answer
}
Note: The program cannot continue until all the questions are answered.
I am aware that JavaScript is single-threaded and cannot block the execution of the code, which is why I am having difficulty implementing this.

What you are really after is an Electron method similar to ipcRenderer.invoke but instead of "render to main and back
again", you want "main to render and back again".
There is no real "wait" command one can easily implement in this instance unless you start using promises and a while
or do / while loop.
Instead of iterating over the questions like that, how about comparing the number of answers received to the total
number of questions. When all the questions have been asked / answers received, then move on to the next step in your
application.
main.js (main process)
'use strict';
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
// Prevent garbage collection
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();
askQuestion(); // <--- Ask first question upon window creation
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
const questions = [
{'question': 'Question 1'},
{'question': 'Question 2'},
{'question': 'Question 3'}
]
let answers = [];
electronIpcMain.on('submitAnswer', (event, answer) => {
// Process answer.
answers.push(answer);
if (answers.length < questions.length) {
askQuestion();
} else {
console.log('All questions answered');
console.log(answers);
}
})
function askQuestion() {
window.webContents.send('askQuestion', questions[answers.length].question);
}
preload.js (main process)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld(
'electronAPI', {
// From main to render
askQuestion: (question) => {
ipcRenderer.on('askQuestion', question)
},
// From render to main
submitAnswer: (answer) => {
ipcRenderer.send('submitAnswer', answer)
}
});
index.html (main 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>
<div>
<h2>Question</h2>
<div id="question"></div>
</div>
<div>
<h2>Answer</h2>
<textarea id="answer" rows="8" cols="50"></textarea>
</div>
<input type="button" id="submit" value="Submit">
</body>
<script>
let questionField = document.getElementById('question');
let answerField = document.getElementById('answer');
window.electronAPI.askQuestion((event, question) => {
answerField.value = '';
questionField.innerText = question;
})
document.getElementById('submit').addEventListener('click', () => {
window.electronAPI.submitAnswer(answerField.value);
})
</script>
</html>

Related

electronJS how to get data across from preload to renderer

I am trying to send data from a main window to another window in electronJS.
How my app works is there is a main window with many selections. On clicking each selection, a new window will open, and the window will show data that is related to that selection. For now, what works is that clicking each selection will open a new window, but I am unable to pass data over to the new window.
I have read through the electron docs but most seem to be focused on data from renderer to main. The example which shows data passing from main to renderer didn't help me and I still struggle to implement what I want.
I tried looking for some help here
Trying to send data from one electron window to another via ipc
Electron: How to securely inject global variable into BrowserWindow / BrowserView?
Electron: How to pass the message/data from preload to renderer?
and tried to implement the suggestions but I still can't get it to work.
I have 2 html files (index.html and details.html), a main.js, a preload.js and a renderer.js for the details.html
Here are my codes:
main.js
// main.js
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
height: 1000,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
mainWindow.webContents.openDevTools()
}
// 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.whenReady().then(() => {
createWindow()
ipcMain.on('open-selection-window', (event) => {
openNewWindow()
})
app.on('activate', () => {
// On macOS 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 (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
const openNewWindow = () => {
const Window = new BrowserWindow({
width: 1000,
height: 1000,
title: ' details',
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
Window.loadFile('details.html')
}
preload.js (note the contextbridge portion)
// preload.js
const axios = require('axios');
const { contextBridge, ipcRenderer } = require('electron');
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
// this function is called when the user clicks on a selection, it will get the details
const getselectionDetail = (argument) => {
axios.get(`http://apiurl/${argument}`)
.then(response => {
return response.data;
})
}
// this function is called when user press search button, it will search for the selections thru API call,
// and then display the results,
// set onclick function for each result,
const searchselections = (text) => {
//use axios to make a get request to the url
axios.get(`http://apiurl/${text}`)
.then(response => {
const selections = response.data;
// for each element in selections, append a div with the class of search-result and append the html
selections.forEach(selection => {
document.getElementById('results').innerHTML += `
<div class="search-result">
<p>${selection.name}</p>
</div>`;
});
// for each search result, need to set it such that on click, the contextbridge will send the selection details to the renderer
// and then the renderer will load the selection details
const searchResults = document.getElementsByClassName('search-result');
for (let i = 0; i < searchResults.length; i++) {
searchResults[i].onclick = () => {
contextBridge.exposeInMainWorld(
'selection',
// this is to get a new window to open and sends data to the main process
ipcRenderer.send('open-selection-window', getselectionDetail(selections[i].name))
);
// send data to the renderer -> this doesn't work?
contextBridge.exposeInMainWorld(
'details',
getselectionDetail(selections[i].name)
)
}
}
})
.catch(error => {
console.log(error);
}
)
}
renderer.js
const detail_name = document.getElementById('detail-name');
// load the data from window, need to append to html
console.log(window.details) <-- this doesn't work
index.html
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> -->
<!-- <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"> -->
<title>Search function</title>
</head>
<body>
<h1>Search function</h1>
<div class="root">
<!-- text input for the search -->
<input type="text" id="search-input" placeholder="Search">
<!-- search button -->
<button id="search-button" class="search-button" type="button">
Search
</button>
</div>
<!-- div to display the search results -->
<div id="results"></div>
</body>
</html>
details.html
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> -->
<!-- <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"> -->
<title>Details</title>
</head>
<body>
<h1>Details</h1>
<div class="result">
<!-- div for name -->
<div class="name">
<h5>Name:</h5>
<p id="detail-name"></p>
</div>
</div>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
I appreciate any tips/guidance!
You can use BroadcastChannel like below
const qr = new BroadcastChannel("test");
qr.postMessage(JSON.stringify({ var: "val" }));
and listen from another file like
const bc = new BroadcastChannel("test");
bc.onmessage = (event) => {
this.data = JSON.parse(event.data);
};
You can read more about this on mdn.broadcastchannel
The comments can't hold so much text so I'll post the code here to show what I've tried, but doesn't work.
I added the top code in the onclick assignment
preload.js
// for each search result, need to set it such that on click, the contextbridge will send the selection details to the renderer
// and then the renderer will load the selection details
const searchResults = document.getElementsByClassName('search-result');
for (let i = 0; i < searchResults.length; i++) {
searchResults[i].onclick = () => {
contextBridge.exposeInMainWorld(
'selection',
// this is to get a new window to open and sends data to the main process
ipcRenderer.send('open-selection-window', getselectionDetail(selections[i].name))
);
const qr = new BroadcastChannel("test");
qr.postMessage(JSON.stringify({ var: "val" }));
}
}
renderer.js
// set windows onload event
window.addEventListener('load', () => {
const bc = new BroadcastChannel("test");
bc.onmessage = (event) => {
this.data = JSON.parse(event.data);
console.log(JSON.parse(event.data));
};
});
I think you are trying to do too much in your preload.js script. Additionally, I don't think you can
use contextBridge.exposeInMainWorld more than once per window.
Place your Axios calls in your main process. This will dramatically simplify your preload.js script.
Additionally, just use your preload.js script to transfer data between processed. That way, it will simplify it even
further.
Use your html files only to display the UI, make it interactive and dynamically render content. The remainder can be
handled in the main process.
Note: I have mocked your Axios calls with simplified fake data for testing.
Apart from your main.js file doing the usual things, listen via IPC calls from:
index.html to initiate a search (via invoke) and return the results (via handle).
index.html to open details.html window and send "details" to new window.
If your Axios calls returns promises, see the bottom of the preload.js script for IPC promise use.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
// Prevent garbage collection
let searchWindow;
let detailsWindow;
function createSearchWindow() {
const searchWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 1000,
height: 1000,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
searchWindow.loadFile('index.html')
.then(() => { searchWindow.show(); });
return searchWindow;
}
function createDetailsWindow() {
const detailsWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 300,
height: 300,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
detailsWindow.loadFile('details.html')
.then(() => { detailsWindow.show(); });
return detailsWindow;
}
electronApp.on('ready', () => {
searchWindow = createSearchWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createSearchWindow();
}
});
// ---
electronIpcMain.handle('search', (event, data) => {
// Use "data" variable for Axios call.
return [
'Result 1',
'Result 2',
'Result 3',
'Result 4',
'Result 5'
]
})
electronIpcMain.on('openDetailsWindow', (event, data) => {
// Use "data" for Axios call.
let results = {
'Result 1': 'Details for Result 1',
'Result 2': 'Details for Result 2',
'Result 3': 'Details for Result 3',
'Result 4': 'Details for Result 4',
'Result 5': 'Details for Result 5'
}
detailsWindow = createDetailsWindow();
detailsWindow.webContents.send('pushDetails', results[data]);
})
I have used a white listed channel name configuration for your preload.js script. This simplifies things even further
and prevent your preload.js script from being used for more than just simple channel name calling and data transfer.
If you are after an alternative design for your preload.js script, just let me know.
preload.js (main process)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'openDetailsWindow'
],
// From main to render.
'receive': [
'pushDetails'
],
// From render to main and back again.
'sendReceive': [
'search'
]
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
/**
* Render --> Main
* ---------------
* Render: window.ipcRender.send('channel', data); // Data is optional.
* Main: electronIpcMain.on('channel', (event, data) => { methodName(data); })
*
* Main --> Render
* ---------------
* Main: windowName.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRender.receive('channel', (data) => { methodName(data); });
*
* Render --> Main (Value) --> Render
* ----------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', (event, data) => { return someMethod(data); });
*
* Render --> Main (Promise) --> Render
* ------------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', async (event, data) => {
* return await promiseName(data)
* .then(() => { return result; })
* });
*/
In your index.html (search) file, place an event listener on the "search" button (and the results div). Once clicked,
send an IPC message to the main thread to retrieve your Axios data call. Once retrieved, dynamically add the results to
the DOM.
On a click in your results div, use event delegation to find the result that was clicked and again, use IPC to
send a message to open up the "details" window.
Note: For simplicity, I have added your required renderer.js code in-between <script> tags.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Search function</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<h1>Search function</h1>
<div class="root">
<input type="text" id="search-input" placeholder="Search">
<input type="button" id="search-button" class="search-button" value="Search">
</div>
<div id="results"></div>
</body>
<script>
let results = document.getElementById('results');
document.getElementById('search-button').addEventListener('click', () => {
window.ipcRender.invoke('search', document.getElementById('search-input').value)
.then((data) => {
let output = '';
for (let item of data) {
output += `<div class="search-results">${item}</div>`;
}
results.innerHTML = output;
})
})
results.addEventListener('click', (event) => {
window.ipcRender.send('openDetailsWindow', event.target.innerText);
})
</script>
</html>
The details.html file fill automatically receive the detailed data from main process upon opening.
details.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Details</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<h1>Details</h1>
<div class="result">
<div class="name">
<h5>Name:</h5>
<p id="detail-name"></p>
</div>
</div>
</body>
<script>
window.ipcRender.receive('pushDetails', (details) => {
document.getElementById('detail-name').innerText = details;
})
</script>
</html>

electron js - cannot get button to perform simple actions from click

Long story short I am working on a single page application that sends commands over a local network. Testing out Electron JS and I can't even seem to get a simple button to work. I feel like I am not linking the logic between main.js and index.js somehow but for the life of me I cannot figure out the correct way to do it. I have even put breakpoints in index.js and through main.js & index.html but none of the breakpoints are hit aside from the ones in main.js. I put a simple function in a preload.js file and that function is correctly called but the one I am trying to attach to a button located in index.html and index.js is never even being hit. A lot of the commented out code is things I want to remember or things I have noticed a different method of creating and just wanted to try and see if that worked. If anyone has any answers or guidance it would be greatly appreciated! :D
Below is my main.js
//#region ---for dev only | hot reload
try {
require('electron-reloader')(module)
} catch (_) {}
//#endregion
const electron = require('electron');
const {app, BrowserWindow, Menu} = require('electron');
const path = require('path');
const ipcMain = electron.ipcMain;
//#region globals
const SRC_DIR = '/src/'
const IMG_DIR = '/assets/images'
//#endregion
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
//frame: false,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
//Used to auto open dev tools for debugging
//win.openDevTools();
win.loadFile('src/index.html');
// win.loadURL(url.format({
// pathname: path.join(__dirname, 'index.html'),
// protocol: 'file',
// slashes: true
// }));
}
app.whenReady().then(() => {
//nativeTheme.shouldUseDarkColors = true;
createWindow();
})
//closes app processes when window is closed
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
})
var menu = Menu.buildFromTemplate([
{
label: 'Menu',
submenu: [
{label: 'Edit'},
{type: 'separator'},
{
label: 'Exit',
click() {
app.quit();
}
}
]
}
])
Menu.setApplicationMenu(menu);
Here is index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Ecas Software</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<p id="myText">Let's get started :)</p>
<button id="myBtn">Change Text</button>
<script type="text/javascript" src="./index.js" ></script>
</body>
</html>
Lastly here is my index.js (aka my first and only renderer?)
const electron = require('electron');
const chgBtn = document.getElementById('myBtn');
function replaceText(selector, text){
const element = document.getElementById(selector);
if (element) element.innerText = text;
}
chgBtn.onclick = function() {
replaceText('myText', 'no boom...');
}
// chgBtn.addEventListener('click', function(){
// // if (document.getElementById('myText').innerText == 'boom'){
// // replaceText('myText','no boom...');
// // } else {
// // replaceText('myText','boom');
// // }
// document.alert("working function");
// });
//chgBtn.addEventListener('click', replaceText('myText','no boom...'));
Why you have this error
The problem here is that you didn't use your scripts files the way Electron was intended.
If you use the Devtools Console (by uncommenting win.openDevTools()), you should see this error in your console :
Uncaught ReferenceError: require is not defined (from index.js file)
This is because your index.js file is loaded as a "normal javascript file". If you want to use the Node syntaxe (aka the "require" syntaxe), you need to do it in your preload script. Only the preload script can use the require syntaxe, since it is the only script allowed by Electron to use Node.
You can also use other javascripts files, by import it in your HTML as you did for the index.js file, but you should remove the require call. As the "require" call (on the first line) will throw and error, all the following code will not run. This is why your button did not react on click.
The correct way to do it
If you need to use some methods from the Electron Renderer API (such as the ipcRenderer), you need to put it in your preload script.
If you want to use your own script, in a separate file, you can also do it, you will not be able to directly call Electron API. There is a solution if you want to call the Electron API in your own script, it is called the Context Bridge. This allows you to create an object in your preload script, that can use the Electron API. You can give this object a name, and then call it from your others script by using the window global object.
For example, if you want to use ipcRenderer.send(channel, payload) :
// Preload script
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('theNameYouWant',
{
send: (channel, payload) => ipcRenderer.send(channel, payload)
}
)
// index.js file, imported in your HTML file
window.theNameYouWant.send("channel-name", { someData: "Hello" })
In your example
// Add this in your main.js file to see when a user click on the button from main process
ipcMain.on("button-clicked", (event, data) => console.log(data))
// Preload script
const { contextBridge, ipcRenderer } = require("electron")
contextBridge.exposeInMainWorld("electron", {
send: (channel, payload) => ipcRenderer.send(channel, payload),
})
// index.js
const chgBtn = document.getElementById("myBtn")
function replaceText(selector, text) {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
chgBtn.onclick = function () {
replaceText("myText", "no boom...")
window.electron.send("button-clicked", { someData: "Hello" })
}

Using the electron ipcRenderer from a front-end javascript file

I'm in the process of learning to use Electron, and while trying to have my application communicate with the front end I am aware I need to use the ipcRenderer to gain a reference to the DOM elements and then pass that information to ipcMain.
I tried to follow much of the advice suggested here and here, but both of these examples use require('electron').ipcMain and whenever I try to include my script that will be interacting with the front-end into my HTML, nothing occurs since Uncaught ReferenceError: require is not defined. I've been searching for a few hours and haven't had any luck finding a solution - so clearly I'm doing something wrong.
My main.js is very simple, I just create my window and then I create an ipc listener as so:
const { app, BrowserWindow } = require("electron");
const ipc = require('electron').ipcMain;
function createWindow() {
const window = new BrowserWindow({
transparent: true,
frame: false,
resizable: false,
center: true,
width: 410,
height: 550,
});
window.loadFile("index.html");
}
app.whenReady().then(createWindow);
ipc.on('invokeAction', (event, data) => {
var result = "test result!";
event.sender.send('actionReply', result);
})
Within the file that I wish to manipulate the DOM with, I attempt to get the element ID and then add an event listener as seen here:
const ipc = require('electron').ipcRenderer;
const helper = require("./api");
var authenticate_button = ipcRenderer.getElementById("authenticate-button");
var authButton = document.getElementById("authenticate-button");
authButton.addEventListener("click", () => {
ipc.once('actionReply', (event, response) => {
console.log("Hello world!");
})
ipc.send('invokeAction');
});
function onAuthenticateClick() {
helper.authenticateLogin(api_public, api_secret, access_public, access_secret);
}
and finally, my HTML only consists of a button that I wish to attach my event listener to:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project Test</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="main-container">
<button id="authenticate-button" type="submit" onclick="">Authenticate</button>
<p id="status-label">Not Authenticated</p>
</div>
<script src="script.js"></script>
</body>
</html>
If anyone could help point me in the right direction as to how to get this basic functionality to work, it would be very helpful!
As mentioned by AlekseyHoffman, the reason you can't access ipcRenderer in your frontend js file is because you have nodeIntegration set to false. That said, there's a reason it's set to false by default now; it makes your app far less secure.
Let me suggest an alternate approach: rather than trying to access ipcRenderer directly from your frontend js by setting nodeIntegration to true, access it from preload.js. In preload.js, you can selectively expose ipcMain functions (from your main.js file) you want to access on the frontend (including those that can send data back from main.js), and call them via ipcRenderer there. In your frontend js, you can access the preload.js object that exposes those functions; preload.js will then call those main.js functions via ipcRenderer and return the data back to the frontend js that called it.
Here's a simple, but fully working example (these files should be sufficient to build an electron app with two-way communication between main.js and frontend. In this example, all of the following files are in the same directory.):
main.js
// boilerplate code for electron..
const {
app,
BrowserWindow,
ipcMain,
contextBridge
} = require("electron");
const path = require("path");
let win;
/**
* make the electron window, and make preload.js accessible to the js
* running inside it (this will allow you to communicate with main.js
* from the frontend).
*/
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false,
preload: path.join(__dirname, "./preload.js") // path to your preload.js file
}
});
// Load app
win.loadFile(path.join(__dirname, "index.html"));
}
app.on("ready", createWindow);
// end boilerplate code... now on to your stuff
/**
* FUNCTION YOU WANT ACCESS TO ON THE FRONTEND
*/
ipcMain.handle('myfunc', async (event, arg) => {
return new Promise(function(resolve, reject) {
// do stuff
if (true) {
resolve("this worked!");
} else {
reject("this didn't work!");
}
});
});
Note, I'm using an example of ipcMain.handle because it allows two-way communication and returns a Promise object - i.e., when you access this function from the frontend via preload.js, you can get that Promise back with the data inside it.
preload.js:
// boilerplate code for electron...
const {
contextBridge,
ipcRenderer
} = require("electron");
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
// end boilerplate code, on to your stuff..
/**
* HERE YOU WILL EXPOSE YOUR 'myfunc' FROM main.js
* TO THE FRONTEND.
* (remember in main.js, you're putting preload.js
* in the electron window? your frontend js will be able
* to access this stuff as a result.
*/
contextBridge.exposeInMainWorld(
"api", {
invoke: (channel, data) => {
let validChannels = ["myfunc"]; // list of ipcMain.handle channels you want access in frontend to
if (validChannels.includes(channel)) {
// ipcRenderer.invoke accesses ipcMain.handle channels like 'myfunc'
// make sure to include this return statement or you won't get your Promise back
return ipcRenderer.invoke(channel, data);
}
},
}
);
renderer process (i.e. your frontend js file - I'll call it frontend.js):
// call your main.js function here
console.log("I'm going to call main.js's 'myfunc'");
window.api.invoke('myfunc', [1,2,3])
.then(function(res) {
console.log(res); // will print "This worked!" to the browser console
})
.catch(function(err) {
console.error(err); // will print "This didn't work!" to the browser console.
});
index.html
<!DOCTYPE html>
<html>
<head>
<title>My Electron App</title>
</head>
<body>
<h1>Hello Beautiful World</h1>
<script src="frontend.js"></script> <!-- load your frontend script -->
</body>
</html>
package.json
{
"name": "myapp",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
The files above should be sufficient to have a fully working electron app with communication between main.js and the frontend js. Put them all in one directory with the names main.js, preload.js, frontend.js, and index.html, and package.json and launch your electron app using npm start. Note that in this example I am storing all the files in the same directory; make sure to change these paths to wherever they are stored on your system.
See these links for more info and examples:
Electron documentation on inter-process communication
An overview of why IPC is needed and the security issues of setting nodeintegration to true
The require is not defined because you didn't enable nodeIntegration on the window. Set it to true in your window config:
const window = new BrowserWindow({
transparent: true,
frame: false,
resizable: false,
center: true,
width: 410,
height: 550,
webPreferences: {
nodeIntegration: true
}
})

Electron, Creating Duplicate of Main Window On Button Click

I am making an electron app and the user to be able to open as many instances of the main window (the one that opens by default) as they would like.
I would like them to be able to do this by simply clicking a button within the index.html.
How would this be possible within the following default apps code ?
main.js
const { app, BrowserWindow } = require('electron')
function createWindow () {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
win.loadFile('index.html')
// Open the DevTools.
win.webContents.openDevTools()
}
// 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.whenReady().then(createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS 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 macOS 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 (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
<button onclick="openWindow()">click to open new window</button>
</body>
</html>
All you need to do is send a message to your main process when you click your button, then in main.js create a new window whenever we receive that message.
So first send a message to main.js from your openWindow() function like this:
var ipcRenderer = require('electron').ipcRenderer;
function openWindow () {
ipcRenderer.send('asynchronous-message', 'createNewWindow');
}
Then we listen for the message in main.js like this:
var ipcMain = require('electron').ipcMain;
ipcMain.on('asynchronous-message', function (evt, message) {
if (message == 'createNewWindow') {
// Message received.
// Create new window here.
}
});
Then all you need to do is create a new window when you receive the 'createNewWindow' message.
See docs for sending a message to the main process
See docs for receiving messages in the main process
You can achieve your goal in two ways.
You should change your index.html to like this as pre-requirements
At your index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<body>
<button onclick="require('./renderer.js').openWindow()">click to open new window</button>
</body>
<!-- All of the Node.js APIs are available in this renderer process. -->
We are using Node.js <script>document.write(process.versions.node)</script>,
Chromium <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
<script>
// You can also require other files to run in this process
require('./renderer.js')
</script>
</body>
</html>
Using ipc Communiation.
At your main.js
const {app, BrowserWindow, ipcMain} = require('electron')
let browserWindows = [];
function createWindow () {
// Create the browser window.
let newWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
newWindow.loadFile('index.html')
newWindow.on('closed', function () {
newWindow = null
})
browserWindows.push(newWindow)
}
app.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
ipcMain.on('createNewWindow', (evnet, args) => {
createWindow();
})
At your renderer.js
const { ipcRenderer } = require('electron')
module.exports.openWindow = event => {
ipcRenderer.send('createNewWindow', {});
}
Not using ipc.
you can create directly. Create browserWindow directly at your renderer without ipc
Communitaion. Hence you enable the node api at your renderer. So that
At your renderer.js
const {BrowserWindow} = require('electron').remote
module.exports.openWindow = event => {
const newWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
newWindow.loadFile('index.html')
}
In this way, you don't need to change your main.js

Why is my ipcMain not sending to ipcRenderer in Electron?

New to electron I've figured out how to send from Renderer to Main but I'm trying to learn how to go from Main to Renderer. In my research I've read:
IPC send from main process to renderer and tried:
main.js:
const { app, ipcMain, Menu } = require('electron')
const appVersion = process.env.npm_package_version
const mainWindow = require('./renderer/mainWindow')
app.on('ready', () => {
mainWindow.createWindow(),
console.log(`Trying to send app version to renderer: ${appVersion}`),
mainWindow.webContents.send('app-version', appVersion),
Menu.setApplicationMenu(mainMenu)
})
but I get an error of:
Uncaught Exception TypeError Cannot read property 'send' of undefined
After reading "Send sync message from IpcMain to IpcRenderer - Electron" I tried:
ipcMain.on('app-version', (event) => {
console.log(`Sent: ${appVersion}`)
event.sender.send(appVersion)
}),
but nothing happens or errors out. My renderer.js:
const { ipcRenderer } = require('electron')
ipcRenderer.on('app-version', (event, res) => {
console.log(res)
})
Why is my ipcMain not sending to my ipcRenderer?
Edit:
mainWindow.js:
// Modules
const { BrowserWindow } = require('electron')
// export mainWindow
exports.createWindow = () => {
// BrowserWindow options
// https://electronjs.org/docs/api/browser-window#new-browserwindowoptions
this.win = new BrowserWindow({
minWidth: 400,
minHeight: 400,
frame: false,
webPreferences: {
nodeIntegration: true,
backgroundThrottling: false
}
})
// Devtools
this.win.webContents.openDevTools()
// Load main window content
this.win.loadURL(`file://${__dirname}/index.html`)
// Handle window closed
this.win.on('closed', () => {
this.win = null
})
}
I've also tried:
main.js:
app.on('ready', () => {
mainWindow.createWindow(),
mainWindow.win.webContents.send('app-version', appVersion),
Menu.setApplicationMenu(mainMenu)
})
renderer.js:
console.log("Trying")
ipcRenderer.on('app-version', (args) => {
console.log(`Node version is ${args}`)
})
For some reason now the applied answer I've written ipcMain sends to renderer several times and renders the console message repeatedly
Trying to send app version to renderer: 1.0.0
Trying to send app version to renderer: 1.0.0
Trying to send app version to renderer: 1.0.0
Trying to send app version to renderer: 1.0.0
Trying to send app version to renderer: 1.0.0
Now the application flashes but in the console.log it shows it once and I do not understand why.
Edit
To respond to the answer.
I'm aware of app.getVersion I was just using ENV to learn how to send to main. After testing the code this approach doesn't work.
Coping this:
app.on('ready', () => {
const win = mainWindow.createWindow(),
console.log(`Trying to send app version to renderer: ${appVersion}`),
win.webContents.send('app-version', appVersion),
Menu.setApplicationMenu(mainMenu)
})
throws the error of:
Missing initializer in const declaration
so modified to this:
app.on('ready', () => {
const win = mainWindow.createWindow()
console.log(`Trying to send app version to renderer: ${appVersion}`)
win.webContents.send('app-version', appVersion)
Menu.setApplicationMenu(mainMenu)
})
but when adding the return at the end of exports.createWindow throws this error of:
win is not defined
adding let.win before exports.createWindow will not throw a code but nothing is sent over.
Electron Fiddle
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron learning</title>
<!-- CSS Bootstrap -->
<!-- <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.css"> -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<!--
Font Awesome
https://fontawesome.com/v4.7.0/cheatsheet/
-->
<!-- <link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.css"> -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- Custom CSS -->
<style>
body {
-webkit-app-region: drag;
}
footer {
position: fixed;
bottom: 0;
width: 100%;
}
#close_app,
#site {
cursor: pointer;
}
</style>
</head>
<body class="d-flex flex-column h-100">
<header>
<nav class="navbar navbar-expand navbar-dark fixed-top bg-dark">
<a class="navbar-brand text-white">Foobar</a>
<div class="collapse navbar-collapse justify-content-between">
<div class="navbar-nav">
<a id="site" class="nav-link"><small><u id="application"></u></small></a>
</div>
<div class="navbar-nav">
<a id="close_app" class="nav-item nav-link"><i class="fa fa-times-circle"></i></a>
</div>
</div>
</nav>
</header>
<main role="main" class="flex-shrink-0">
<div class="container">
<h1>Hello World!</h1>
<!-- All of the Node.js APIs are available in this renderer process. -->
We are using Node.js <script>document.write(process.versions.node)</script>,
Chromium <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
Electron app version <script>document.write(process.versions.electron)</script>.
</div>
<p id="testSender"></p>
</main>
<footer class="footer mt-auto py-3 ">
<div class="container">
<span class="text-muted">Place sticky footer content here.</span>
</div>
</footer>
<script>
// jQuery
// window.jQuery = window.$ = $ = require('jquery')
// You can also require other files to run in this process
require('./renderer.js')
</script>
<!-- <script src="../node_modules/jquery/dist/jquery.min.js"></script> -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<!-- <script src="../node_modules/bootstrap/dist/js/bootstrap.min.js"></script> -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>
main.js:
'use strict'
// Modules to control application life and create native browser window
const { app, ipcMain, BrowserWindow, Menu } = require('electron')
const testMainSend = `Trying to send something to renderer`
let mainWindow
// Window state keeper
const windowStateKeeper = require('electron-window-state')
// export mainWindow
function createWindow () {
let winState = windowStateKeeper({
defaultWidth: 400,
defaultHeight: 400
})
// BrowserWindow options
// https://electronjs.org/docs/api/browser-window#new-browserwindowoptions
const win = new BrowserWindow({
width: winState.width,
height: winState.Height,
x: winState.x,
y: winState.y,
minWidth: 400,
minHeight: 400,
frame: false,
webPreferences: {
nodeIntegration: true,
backgroundThrottling: false
}
})
winState.manage(win)
// Devtools
win.webContents.openDevTools()
// Load main window content
win.loadURL(`file://${__dirname}/index.html`)
// Handle window closed
win.on('closed', () => {
this.win = null
})
return win
}
// 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()
// 1st attempt
// webContents.send('misc-sender', testMainSend)
// 2nd attempt
// const win = createWindow()
// win.webContents.send('misc-sender', testMainSend)
// 3rd attempt
const win = createWindow()
win.webContents.on('dom-ready', () => {
console.log(`Trying to send renderer: ${testMainSend}`)
mainWindow.win.webContents.send('misc-sender', testMainSend)
})
})
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS 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 macOS 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()
}
})
// Close application from button
ipcMain.on('closing-app', () => {
app.quit()
console.log('Closed app from font awesome link')
})
renderer.js:
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// All of the Node.js APIs are available in this process.
const { ipcRenderer, shell } = require('electron')
const appVersion = require('electron').remote.app.getVersion()
// Devtron
// require('devtron').install()
// Close App
const closeApp = document.getElementById('close_app')
closeApp.addEventListener('click', () => {
ipcRenderer.send('closing-app')
})
// received from ipcMain test
ipcRenderer.on('misc-sender', (event, args) => {
appendTest = document.getElementById('testSender')
appendTest.innerHTML += args
})
// Getting version
const appVersioning = document.getElementById('application')
appVersioning.innerHTML = appVersion
// Open site
const homeURL = document.getElementById('site')
homeURL.addEventListener('click', (e) => {
e.preventDefault
shell.openExternal("https://www.google.com/")
})
After several searches and attempts I think I've finally figured out how to send my application version from package.json to main then to the renderer. My issue was in my app.on I was missing dom-ready which helped after reading IPC Communication not working between Electron and window:
main.js:
const appVersion = process.env.npm_package_version
app.on('ready', () => {
mainWindow.createWindow()
Menu.setApplicationMenu(mainMenu)
// Send version to renderer
mainWindow.win.webContents.on('dom-ready', () => {
console.log(`Trying to send app version to renderer: ${appVersion}`)
mainWindow.win.webContents.send('app-version', appVersion)
})
})
renderer.js:
ipcRenderer.on('app-version', (event, args) => {
const appVersion = document.getElementById('app_version')
console.log(`Node version is ${args}`)
appVersion.innerHTML += args
})
index.html:
<div id="app_version"></div>
There might be a better way to do this but after further research I read:
Electron - How to know when renderer window is ready
dom-ready from instance-events
and this works but next steps are to see if pulling the process.env is a good security practice. I do hope to see some other answers on a possible better approach if it exists.
Before answering your question, looking at the message you are sending to the renderer ( if what you want to do is send your application version to the render process ) it can be done with app.getVersion. From your main process you have to set the app version with app.setVersion("1.0") and then from your render process you have to do this
const { remote: { app } } = require("electron");
app.getVersion();
To your actual question mainWindow.createWindow() needs to return an instance of BrowserWindow ( looking at your code you are not returning it and also you are not reading the win property object you set on mainWindow.js ). If you want to stick with the current code in your question you have to do this
mainWindow.win.webContents.send(...)
or you should do this from
mainWindow.js
// Modules
const { BrowserWindow } = require('electron')
// export mainWindow
exports.createWindow = () => {
// BrowserWindow options
// https://electronjs.org/docs/api/browser-window#new-browserwindowoptions
const win = new BrowserWindow({
minWidth: 400,
minHeight: 400,
frame: false,
webPreferences: {
nodeIntegration: true,
backgroundThrottling: false
}
})
// Devtools
win.webContents.openDevTools()
// Load main window content
win.loadURL(`file://${__dirname}/index.html`)
// Handle window closed
win.on('closed', () => {
this.win = null
})
return win;
}
main.js
const { app, ipcMain, Menu } = require('electron')
const appVersion = process.env.npm_package_version
const mainWindow = require('./renderer/mainWindow')
app.on('ready', () => {
const win = mainWindow.createWindow(),
console.log(`Trying to send app version to renderer: ${appVersion}`),
win.webContents.send('app-version', appVersion),
Menu.setApplicationMenu(mainMenu)
})

Categories

Resources