electron js how to call a function rendered in index - javascript

I'm going crazy to be able to call a function that I have rendered in an index and then execute that function with globalShortcut, I was researching and found that IPC could perform, but all the examples are to use functions from the main and not from the index. any idea that can guide me

I assume you have nodeIntegration set to true and contextIsolation set to false, as advised by the Electron documentation. To trigger a function in your renderer process from the main thread you can use ipcMain and ipcRenderer (these modules are part of Electron).
First, you need to expose an API for the renderer using contextBridge. This can be done with a preload.js file that has access to all modules like the main thread.
Example:
main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createMainWindow(){
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js'),
},
});
}
app.on('ready', () => {
createMainWindow();
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => {
// Whitelist channels
let validChannels = ['toMain'];
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));
}
},
});
Then, in your renderer process you can use window.api.send(channel, parameters); to send messages to the main thread and window.api.receive(channel, parameters); to listen for incoming ones. Similarly, you would use ipcMain.on(channel, (event, parameters) => { }); in the main process to handle messages from the renderer. However, to directly send messages to it, you have to use webContents, like so: mainWindow.webContents.send(channel, parameters);.
In your case, to execute a function in the renderer, you would call mainWindow.webContents.send('fromMain', 'trigger-function'); in the main process; and listen for this event in your renderer like so:
window.api.receive('fromMain', (parameters) => {
if (parameters === 'trigger-function'){
// Call your renderer function here
}
});

Related

Sending messages through Electron IPCmain channel to vue instance only works one way

I'm tyring to send messages back and forth between the main electron process and the vue instance. What i have so far is
Preload.js:
import { contextBridge, ipcRenderer } from 'electron'
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
window.ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld('ipcRenderer', {
//Render (Vue) to main (Electron)
send: (channel, data) => {
let validChannels = ['clientMessage'] // <-- Array of all ipcRenderer Channels used in the client
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
//Main (Electron) to Render (Vue)
on: (channel, func) => {
let validChannels = ['electronMessage'] // <-- Array of all ipcMain Channels used in the electron
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
So i have 2 whitelisted channels in ipcRender, one is called 'clientMessage' to send messaged from the vue instance to the electron main process, the other is 'electronMessage' to send messages from the electron main process to the vue instance.
In my background.js i have the following:
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
ipcMain.on('clientMessage', (event, args) => {
console.log('received a message from vue: '+args)
event.sender.send('message','return Message from electron'+args);
});
And in my app.Vue i have:
mounted () {
window.ipcRenderer.on('electronMessage', (event, data) => {
console.log('message from electron: '+data)
})
},
methods: {
sendMessage(){
window.ipcRenderer.send('clientMessage','testing')
}
}
The app runs fine, and when i call the sendMessage function I correctly get a console log on the electron terminal saying message received. So clearly vue -> electron messaging has worked, but why wont it work for the reverse?
Your contextBridge.exposeInMainWorld "keys" are ipcRenderer.send and ipcRenderer.receive.
You must use these keys to access your defined preload.js IPC methods. IE: send and on.
Specifically, to use the ipcRender.on(...) method, you call it with window.ipcRenderer.receive(...).
app.Vue (render thread)
// To send a message from render thread to main thread.
window.ipcRenderer.send('clientMessage','testing'); // Working
// To receive a message from main thread to render thread.
window.ipcRenderer.receive('electronMessage', (event, data) => {
console.log('message from electron: ' + data);
});
I figured out why, the way to send messages from electron main process to the renderer instance is not the same. Since there is only one electron main process running at all times, we can simple do window.ipcRenderer.send('', 'your message') in the vue renderer instance, but since there can be more than one renderer instances running, the main electron process needs to know which render process to send it to.
So in your main.js/background.js whatever .js you are running the electron main process out of. do this:
let win;
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: true,
preload: path.join(__dirname, "preload.js")
}
})
.
.
.
and then to send a message to the render instance:
win.webContents.send('<channelName>','your message')
that way it sends it to the correct render process. So my code in the end looks like this:
ipcMain.on('clientMessage', (event, args) => {
console.log('received a message from vue: '+args)
win.webContents.send('electronMessage','Reply from main process: '+args)
});

Sending message from Main to Renderer

Could someone help me out here. I'm completely confused on how to solve this. I've now spent about a week trying to find a solution for this but have come up short and there appears to be a lack of a solid solution online. I've made a github repository trying to demonstrate the issue.
In short I've implemented a status bar in my application which i want to populate with various string messages. These messages would be sent from functions that are contained inside a js file that imports electron, which mean's it doesn't have direct access to the Renderer. So how would I send these messages to the Renderer. I'm assuming this needs to be done using the ContextBridge, but i have no clue how to successfully do this, so if your response is just linking me to the context bridge docs, don't bother, lol I've exhausted myself looking at that. The other alternative i was considering is using a custom event but but i'm not sure that would solve the problem either.
Here is a sample of what im trying to do along with repo on github. If you do a pull-request to fix the repo, ill gladly merge and keep the repo public for others to benefit from and share with the community. https://github.com/JokerMartini/statusbar
As a minor problem, im not sure why i can no longer call getPath from 'app' from within a js file that's not loaded into the render thread.
I trigger a method from Renderer
index.vue
const doWork = () => {
window.messenger.doWork();
}
electron-preload.js
import { contextBridge } from "electron";
const messenger = require("../src/helpers/messenger");
contextBridge.exposeInMainWorld("messenger", messenger);
messenger.js
const { app } = require("electron");
const path = require("path");
// using electron module to demonstrate this file can't be imported into renderer
export function showMessage(msg) {
const dir = path.join(app.getPath("documents"), "presets");
console.log(dir);
// TODO: send message to renderer...
}
export function doWork() {
console.log("Doing working...");
// step 1: long process
showMessage("Processing step 1...");
// step 2: long process
showMessage("Processing step 2...");
// step 3: long process
showMessage("Processing step 3...");
}
I'd like to display the messages sent from the main to renderer to be displayed in the status bar of
main.vue
<q-footer>
<q-bar>
<span class="text-caption">Show message here...</span>
</q-bar>
</q-footer>
** UPDATE 01 **
For some reason my message is not being received in the Renderer. Here are my code changes
electron-preload.js
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("electronAPI", {
setStatus: (callback, func) =>
ipcRenderer.on("set-status", (event, ...args) => func(...args)),
});
index.vue
<template>
<q-page class="flex flex-center">
<q-btn label="Show Message" #click="doWork" />
</q-page>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
setup() {
// send message for testing...
const doWork = () => {
window.electronAPI.setStatus("sfsfsdfsd");
};
// recieve status messages...
window.electronAPI.setStatus("set-status", (data) => {
console.log("STATUS:", data);
// TODO $store.dispatch("....");
});
return {
doWork,
};
},
});
</script>
A technique that works for me is not to use the preload.js script to define concrete implementations. Instead, I use the preload.js script to only define channel (names) that I can communicate with between the main and render threads. IE: Seperating your concerns. Implement your concrete functions within your main thread scripts and render thread scripts.
preload.js
// 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': [],
// From main to render.
'receive': [
'message:update' // Here is your channel name
],
// From render to main and back again.
'sendReceive': []
}
};
// 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);
}
}
}
);
Note: Though I do not use Vue.js, you should get the gist of the below two files.
main.js (main thread)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
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();
// Send a message to the window.
window.webContents.send('message:update', 'Doing work...');
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<span id="text-caption">Show message here...</span>
</body>
<script>
// Listen for message updates from the main thread.
window.ipcRender.receive('message:update', (message) => {
document.getElementById('text-caption').innerText = message;
});
</script>
</html>
You want to be using ipcMain (in the main process) and ipcRenderer (in the renderer process). If you compare your scripts to the examples at https://www.electronjs.org/docs/latest/tutorial/ipc that is what is missing.
(There is a section specifically on doing main to renderer.)
This did use to be clearer and simpler, but carried more potential for abuse. So best to ignore any online tutorials older than a year or so. (Though contextBridge is quite new, so if they mention it then they should be recent.)

Why send method of Electron does not working from the main process?

I'm in big trouble. I checked other solutions but nothing helped me.
I don't understand why the "send" method doesn't work here. No exceptions, no messages or anything else.
I tried wrapping the send method in another method but it failed.
I tried using the "sendToFrame" method but it failed too.
main:
const { app, BrowserWindow, ipcMain } = require('electron')
let win;
function createWindow () {
win = new BrowserWindow({
width: 800,
height: 600,
frame: true,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: true,
contextIsolation: false, // false if you want to run 2e2 test with Spectron
enableRemoteModule: true,
}
});
win.loadFile('src/home.html');
win.webContents.openDevTools();
ipcMain.on('main', (event, msg) => {
sendData() // Does not work.
event.reply('new-client', 'res') // It works.
})
function sendData() {
win.webContents.send('new-client', {wsh: 'wshsdqsdqs'});
}
};
app.whenReady().then(createWindow);
Below, the "ipcRenderer.send" method works. It sends data to the main process but receives no response when I use the "webContents.send" method (in the main process).
new-client.js:
const { ipcRenderer } = require('electron');
const {resolve} = require('path');
const {Message} = require(resolve('src/js/shared/class/message/message.js'));
ipcRenderer.on('new-client', (event, arg) => {
console.log(event)
console.log(arg)
})
async function send() {
const msg = new Message(
'new-client',
'clients',
true
);
ipcRenderer.send('main', msg);
}
EDIT:
I forgot this...
I sent the data to the wrong window.
You can use BrowserWindow.getAllWindows, then send the data to a specific window.
Personally, I added an event when windows are open, in this way I can properly send data and manage windows.

How to properly use preloads and window.api calls in electron?

I am having trouble using a render.js file to write "api" calls from the rendered html page's javascript.
main.js new BrowserWindow function includes:
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
preload: "preload.js" // use a preload script
},
preload.js:
const { ipcRenderer, contextBridge } = require('electron')
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
let validChannels = ["login"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["fromMain"];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
}
}
);
passwordPage.js linked in passwordPage.html:
document.getElementById("login").addEventListener('click', function() {
window.api.send("login", "test");
})
error in console of passwordPage:
Uncaught TypeError: Cannot read property 'send' of undefined
According to docs:
The value should be the absolute file path to the script.
Try to use `${__dirname}/preload.js` or something equal.

Electron: Communicate between BrowserWindow and rendered URL (nodeIntegration: false)

I've spent about an hour reading gist after repo after blog post, but can't seem to figure out how to do do this.
I have a BrowserWindow instance loading a URL (that I control), with nodeIntegration: false.
From the main process, I'd like to communicate with the rendered URL. I'm getting confused between preload scripts, BrowserWindow.send and executeJavascript paradigms.
The data I want to send is very large (eg. file uploads between 50kb and 10mb).
What's the best way to do this? Any any examples/tutorials you may know about would be helpful. Thanks!
main.js
const path = require('path')
const electron = require('electron')
const { app, BrowserWindow, ipcMain } = electron
const window = new BrowserWindow({
minWidth: 1200,
minHeight: 700,
autoHideMenuBar: true,
resizable: true,
show: false,
scrollBounce: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
}
})
window.webContents.loadURL('https://xxx.xxx.com') // load your web page
ipcMain.on('ping', (event, msg) => {
console.log(msg) // msg from web page
window.webContents.send('pong', 'hi web page') // send to web page
})
preload.js
const { ipcRenderer } = require('electron');
function init() {
// add global variables to your web page
window.isElectron = true
window.ipcRenderer = ipcRenderer
}
init();
your web page
<script>
if (window.isElectron) {
window.ipcRenderer.send('ping', 'hello main')
window.ipcRenderer.on('pong', (event, msg) => console.log(msg))
}
</script>
Using preload script should work. You can use ipcRenderer to communicate with main process and expose it with simple API to renderer window. Simplest preload.js can look like:
const { ipcRenderer } = require('electron');
let listener;
const bridge = {
send: data => ipcRenderer.send('from-renderer', data),
onMessage: callback => listener = callback
}
ipcRenderer.on('to-renderer', (event, arg) => {
if (listener) {
listener(arg);
} else {
console.warn('No listener');
}
});
window.bridge = bridge;
in renderer
window.bridge.send('Data to main process');
window.bridge.onMessage(payload => console.log('Data received', payload))
Please also take a look at this discussion to get more info.

Categories

Resources