Converting a React + Redux + MUI Menu to Electron - javascript

I am attempting to convert a web-based React + Redux + MUI app to Electron. The app has a main AppBar with multiple dropdown menus, with menu items which hook into the app's Redux store. So a fairly typical set-up for these technologies in partnership, but I'm struggling to understand how to translate this (if it's possible) to an Electron application menu.
So if I have a MUI MenuItem with a typical onClick handler, like this:
const [soneState, setSomeState] = useState();
const handleOnClick = (e) => {
const val = e.target.value;
console.log(`The value is ${val}`);
setSomeState(val);
}
What would be the equivalent for an Electron Menu? Assuming also that I am leveraging the Redux store, rather than local component state. A number of the handlers in the menu communicate with an Express server via fetch. I have been reading up on Electron inter-process communication via the contextBridge, but I'm not sure which side the Electron Menu comes in that equation. Can it leverage both the Redux store as well as talking to the Main process? I assume I can't make fetch calls from the Menu?

OK I think I figured it out...
I can use the pattern described in the official docs here to create an API in my preload.js, which the app menu can then call into to send messages to the render process. So in preload.js I have:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
loadSomeFile: (callback) => ipcRenderer.on('load-some-file', callback),
})
Then I define my menu like this:
const { app, Menu, MenuItem, dialog, ipcMain } = require('electron');
module.exports = (window) => {
return Menu.buildFromTemplate([
{
label: 'File',
submenu: [
{
label: 'About',
},
{
label: 'Preferences',
},
{
type: 'separator'
},
{
label: 'Load Some File',
click() {
dialog.showOpenDialog({
properties: ['openFile']
})
.then((fileObj) => {
window.webContents.send('load-some-file', fileObj);
})
}
},
{
type: 'separator'
},
{
label: 'Exit',
click() {
app.quit()
}
}
]
},
])
}
And in my main.js I have:
const { app, BrowserWindow, Menu, } = require('electron');
const path = require("path");
const mainMenu = require("./app/main-menu");
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
win.loadURL("http://localhost:8080");
return win;
}
app.whenReady().then(() => {
const win = createWindow();
Menu.setApplicationMenu(mainMenu(win));
});
Most of what I already have in my React/Redux/MUI frontend code can remain unchanged. But I need to access the api from the window object on the frontend, and listen to events coming from the Main process (like from the app menu).
window.api.loadSomeFile((event, data) => {
console.log(data);
});
Tested it and it works well. Very little to actually change in my code.

Related

Function onLoad in SPA Page

I have SPA page, all work very good but when user reload page beeing on winners or garage get info :
Cannot GET /Garage. Then have to pick default url. How to set reload function on current page.
https://darogawlik-async-race-api.netlify.app/ (my app)
const navigateTo = url => {
history.pushState(null, null, url)
router()
}
const router = async () => {
const routes = [
{ path: '/Garage', view: garage },
{ path: '/Winners', view: winners },
]
// Test each route for potential match
const potentialMatches = routes.map(route => ({
route,
isMatch: location.pathname === route.path,
}))
let match = potentialMatches.find(potentialMatches => potentialMatches.isMatch)
if (!match) {
match = {
route: routes[0],
isMatch: true,
}
}
const view = new match.route.view(document.querySelector('#main'))
}
window.addEventListener('popstate', router)
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', e => {
if (e.target.matches('[data-link]')) {
e.preventDefault()
navigateTo(e.target.href)
}
})
router()
})
window.addEventListener('load', router())
This will be a problem with default document handling in the web host - it is not a page load problem. Eg just click this link to get the problem:
https://darogawlik-async-race-api.netlify.app/Garage
Since you are using path based routing, your web host must serve the default document for all paths, including /Garage and /Winners. As an example, in Node.js Express you write code like this. For other web hosts you either write similar code or there is a configuration option that will do it for you.
// Serve static content for physical files, eg .js and .css files
expressApp.use('/', express.static());
// Serve the index.html for other paths
expressApp.get('*', (request, response) => {
response.sendFile('index.html');
}
According to this post on Netlify, you can add a file something like this. I'm no expert on this platform, but hopefully this gives you the info you need to resolve your issue:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

Can't use any methods of an istance of a third-party modules [chess.js] (ELECTRON - preload.js & render.js problem)

Today i faced this problem: i was trying to create a chessboard app (with Electron) using the chess.js modules and the problems showed quickly... I need to use in my render.js file all methods/features that chess.js offers, such as chess.move(), chess.fen() and so on... But after setting sandbox: false (in webPreferences) and writing this in my preload script (preload.js):
const { contextBridge, ipcRenderer} = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
//other code
getChess : () => {
const {Chess} = require('chess.js');
return chess = new Chess;
}
}
)
i am unable to use any the methods (to make things more clearer, i am enable to use the "chess" istance of Chess in my render script, i can log this, and it will show the obj (as expected))
this is my render script (render.js):
//get Chess through preload.js
const chess = window.electron.getChess();
//✅ Working (showing the chess obj)
console.log(chess)
//❌ Not Working (example Uncaught TypeError: chess.move is not a function)
chess.move('e42')
chess.inCheck()
chess.isAttacked()
chess.load()
chess.clear()
chess.board()
chess.fen()
chess.png()
//. . .
Probably is something stupid to ask, but it seems to me hard to get through.
chess.js have a lot of methods .fen, .move, .load ... i need a way to include all to my render.
I don't wanna set nodeIntegration: true due to security reason.
idk if it will help but here is my main.js file :
const { app, BrowserWindow, Menu, ipcMain} = require('electron')
const path = require('path')
const ipc = ipcMain
Menu.setApplicationMenu(false)
const createWindow = () => {
const win = new BrowserWindow({
width : 1500,
height : 900,
resizable: false,
titleBarStyle: 'hidden',
frame: false,
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
devTools: false,
sandbox: false,
preload: path.join(__dirname, 'preload.js'),
}
})
//win.webContents.openDevTools()
win.loadFile("src/index.html");
ipc.on('closeAPP', () => {
win.close()
})
ipc.on('minimizeAPP', () => {
win.minimize()
})
ipc.on('maximizeAPP', () => {
win.maximize()
})
}
app.whenReady().then( () => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
There is a way to use all the methods ?
This is by design. Electron does not support, supposedly for security reasons, sending "custom classes" over the contextBridge (as per the documentation) and drops all prototype modifications or prototypes of objects other than Object itself.
This is because you're expected to expose only certain functions of which you're sure they cannot do harm to the renderer process and instead do all processing either in preload.js (not recommended) or in the main process and pass values via IPC.
Mind that the latter will also not work for non-standard Objects, thus it would be best to store the Chess objects you'll need in the main process, manipulate them there, and only pass the values required for (ex.) display to the renderer process. How this is done best is explained in Electron's IPC tutorial.

getting multiple windows in electronJS which has the same browserwindow instance to display different results

I am trying to send data from a main window to multiple windows 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.
The problem I am facing is that the data does not persist and keeps getting updated each time I click a new selection.
For example:
Main window: selection A,B,C,D
clicks on A
New window 1 pops up, displays data for A
clicks on B
New window 2 pops up, displays data for B. New window 1 displays data for B.
clicks on C
New window 3 pops up, displays data for C. New window 1 & 2 displays data for C.
I think the issue has got to do with the browserwindow instance, but I'm not quite sure how to fix it, because I won't know how many new windows would be opened. Is there a way to 'freeze' the data at the point in time for each instance?
I include my code for both my renderers and main.js. Data is passed from renderer(index) to main and then to renderer(instrument)
main.js
// main.js
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron')
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
height: 1000,
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
})
// 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-instrument-window', (event, instrument) => {
openNewInstrumentWindow()
})
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 openNewInstrumentWindow = () => {
const instrumentWindow = new BrowserWindow({
width: 1000,
height: 1000,
title: 'Instrument details',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
})
instrumentWindow.loadFile('instrument.html')
instrumentWindow.webContents.openDevTools()
// this is where the data is send over to the renderer instrument
ipcMain.on('a', (event, payload) => {
instrumentWindow.webContents.send('your-relayed-message', payload);
});
}
this is my renderer index code. I have extracted the relevant portion. ipcrenderer is used twice, first to open the new window, second is to send the data over to renderer instrument.
const searchResults = document.getElementsByClassName('search-result');
for (let i = 0; i < searchResults.length; i++) {
searchResults[i].onclick = () => {
ipcRenderer.send('open-instrument-window')
ipcRenderer.send('a', instruments[i]);
}
}
this is my renderer instrument code.
const getInstrumentDetail = (exchange) => {
axios.get(`http://url${exchange}`)
.then(response => {
let instrument = response.data;
document.getElementById('instrument-name').innerHTML = instrument.name;
})
}
ipcRenderer.on('your-relayed-message', (event, payload) => {
getInstrumentDetail(payload['exchange']);
});
Appreciate any tips or guidance to point me in the right direction.

Using remote.getGlobal variables in modern isolated renderer process Electron

I'm updating an entire app written a few years ago using Electron v1.8.8. As far as I know, Electron changed its paradigm and now the default expected way of communicating between the main and renderer process is using a module in the middle named preload.js.
To get/set global variables in the old way you would first require the remote module:
var { remote } = require('electron');
And then getting/setting it like so:
remote.getGlobal('sharedObj').get('user')
I've been trying to expose an api object in preload.js to achieve the same old functionality, but with no success.
How I would achieve this without setting nodeIntegration: true, contextIsolation: false?
To send messages between the main thread and a render thread requires the understanding of Inter-Process Communication and Context Isolation.
You are correct in that preload.js acts like a middle man. From my experience, using preload.js as a script whose sole purpose is to communicate data between the main and render threads via the use of defined channels allows for a simple, easy to read, separation of concern. IE: Don't place domain specific functions within your preload.js script. Only add functions that are used for the purpose of performing specific methods of communication.
Below is a typical preload.js file. I have defined a couple of channels to communicate between your main thread and your render thread. PS: You can use any name / naming convention to name your channels.
preload.js (main thread)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'renderThread:saysHi'
],
// From main to render.
'receive': [
'mainThread:saysHi'
],
// 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);
}
}
}
);
Whilst your main.js file may look a little different than that shown below, the main points of interest are the reception and transmission of the defined channels renderThread:saysHi and mainThread:saysHi.
main.js (main thread)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
let appWindow;
function createAppWindow() {
const appWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
fullscreen: false,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
enableLargerThanScreen: true,
closable: true,
focusable: true,
fullscreenable: true,
frame: true,
hasShadow: true,
backgroundColor: '#fff',
show: false,
icon: nodePath.join(__dirname, 'icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
worldSafeExecuteJavaScript: true,
enableRemoteModule: false,
devTools: (! electronApp.isPackaged),
preload: nodePath.join(__dirname, 'preload.js')
}
});
appWindow.loadFile('index.html')
.then(() => {
// Main thread saying hi to the render thread.
appWindow.webContents.send('mainThread:saysHi', 'Hello from the main thread.'); })
.then(() => {
appWindow.show(); })
return appWindow;
}
// Listen for the render thread saying hi.
electronIpcMain.on('renderThread:saysHi', (event, message) => {
console.log(message); });
}
electronApp.on('ready', () => {
appWindow = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Your index.html file will import your Javascript file which would contain the code to listen out and send messages on the specified channels.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="main-thread-message"></div>
<input type="button" id="render-thread-button" value="Click to say hi to the main thread">
</body>
<script type="module" src="index.js"></script>
</html>
index.js (render thread)
let mainThreadMessage = document.getElementById('main-thread-message');
let renderThreadButton = document.getElementById('render-thread-button');
// IIFE - Immediately Invoke Function Expression
(function() {
window.ipcRender.receive('mainThread:saysHi', (message) => {
mainThreadMessage.textContent = message;
});
renderThreadButton.addEventLister('click', () => {
window.ipcRender.send('renderThread:saysHi', 'Hello from the render thread.');
});
})();
In the above code, the global object window contains ipcRender.send and ipcRender.receive which is the structure used in your preload.js script underneath the line contextBridge.exposeInMainWorld. You can rename ipcRender and receive / send / invoke to anything you like, though if you do, you would also use the same reference when using it 'html js' side.

Native Menus not showing OS X Electron

I used the electron-quick-start to create an Electron app, and I want the only native menu to show to be the 'Edit' menu, with the usual suspects inside.
However, after searching and exhausting all relevant Google results for 'electron menu not working', I'm at a loss.
My current main.js file:
const {app, Menu, BrowserWindow} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
app.setName('mathulator');
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 900, height: 550})
// and load the index.html of the app.
mainWindow.loadURL(`file://${__dirname}/index.html`)
// 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 template = [
{
label: 'Mathulator',
submenu: [
{
role: 'quit'
}
]
},
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
},
{
role: 'pasteandmatchstyle'
},
{
role: 'delete'
},
{
role: 'selectall'
}
]
}
]
mainWindow.setMenu(Menu.buildFromTemplate(template))
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// 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', function () {
// 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', function () {
// 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've also packaged it up with electron-packager, to no avail.
I'm running it in the main.js file, which from what I can gather from the masses of either vague or convoluted information around the web, is the main process and therefore one in which I should create the menus.
I also tried doing it in render.js, which I saw suggested. To no avail. It'll either show up with the default electron-quick-start menu, or just a simple menu named after the app, containing one item labelled 'Quit'.
What might I be doing wrong, and what might I have misunderstood from the available information?
Edit: I actually attached the wrong file, tried using Menu.setApplicationMenu() the first time, like so:
const {app, Menu, BrowserWindow} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
app.setName('mathulator');
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 900, height: 550});
// and load the index.html of the app.
mainWindow.loadURL(`file://${__dirname}/index.html`);
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
}
// 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', function () {
// 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', function () {
// 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();
}
})
const template = [
{
label: 'Mathulator',
submenu: [
{
role: 'quit'
}
]
},
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
},
{
role: 'pasteandmatchstyle'
},
{
role: 'delete'
},
{
role: 'selectall'
}
]
}
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
The issue here is that BrowserWindow.setMenu() is only available on Windows and Linux. On macOS you should use Menu.setApplicationMenu().
Note that on OSX the menu is not on the window itself but on the top of dosktop.
I lost bunch of time trying to troubleshoot why it was not showing up.
As #Vadim Macagon stated in comment, make sure that the call to Menu.setApplicationMenu() is in createWindow(). For me it fixed the problem.
maybe you set LSUIElement to 1, that means an agent app, that is, an app that should not appear in the Dock. Change the LSUIElement to 0, the build app's menu will show up.
electron build config
mac: {
icon: 'build/icons/icon.icons',
extendInfo: {
LSUIElement: 0
}
}
detail of LSUIElement is here
https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/20001431-108256

Categories