Why setting innerText in electron preload.js changes DOM - electron

I'm new to electron, a piece of code in the electron tutorial code puzzles me, it should be simple, but I did not find any documentation or article that explains why it's possible.
With context isolation enabled, it is possible to update DOM by setting innerText property of an element.
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])
}
})
It seems impossible to me because Electron documentation says
parameters, errors and return values are copied when they are sent over the bridge
To find out why, I tried to expose a function setElementText into the "main world".
// preload.js
const {contextBridge} = require('electron')
// ...
contextBridge.exposeInMainWorld('bridgeTest', {
setElementText: (element, text) => {
window.abc = 123 // executed in isolated world, won't work
element.innerText = text
}
})
In render.js, call exposed function setElementText.
Execution of this function is proxied so that the body of the function is executed in the "isolated world", I know it because trying to set window.abc, but later in devtools of the window, console.log(window.abc) prints undefined.
Also, if a normal object is passed to setElementText, its innerText property remain unchanged, a similar function setElementText2 defined in the "main world" can change innerText property of the same object.
const setElementText2 = (element, text) => {
element.innerText = text
}
window.addEventListener('DOMContentLoaded', () => {
window.def = 456 // executed in the main world, works
const o = {innerText: 'xyz'}
window.bridgeTest.setElementText(o, 'xxyyzz')
console.log(o.innerText) // xyz
setElementText2(o, 'aabbcc')
console.log(o.innerText) // aabbcc
})
If a DOM element is passed to the exposed setElementText, its innerText property does change, so does the DOM content
window.addEventListener('DOMContentLoaded', () => {
window.def = 456 // executed in main world, works
const helloElement = document.getElementById('hello')
window.bridgeTest.setElementText(
helloElement, 'Hello World')
console.log(helloElement.innerText) // Hello World
})
All source code:
main.js
// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
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()
app.on('activate', function () {
// 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', function () {
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.
preload.js
const {contextBridge} = 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])
}
})
contextBridge.exposeInMainWorld('bridgeTest', {
setElementText: (element, text) => {
window.abc = 123 // executed in isolated world, won't work
element.innerText = text
}
})
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'; style-src 'self' 'unsafe-inline'">
<link href="./styles.css" rel="stylesheet">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
<p id="hello">abcdefg</p>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
render.js
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process because
// `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering
// process.
const setElementText2 = (element, text) => {
element.innerText = text
}
window.addEventListener('DOMContentLoaded', () => {
const helloElement = document.getElementById('hello')
window.def = 456 // executed in main world, works
window.bridgeTest.setElementText(
helloElement, 'Hello World')
console.log(helloElement.innerText) // Hello World
const o = {innerText: 'xyz'}
window.bridgeTest.setElementText(o, 'xxyyzz')
console.log(o.innerText) // xyz
setElementText2(o, 'aabbcc')
console.log(o.innerText) // aabbcc
})
style.css
/* styles.css */
/* Add styles here to customize the appearance of your app */

Related

Electron build is not performing how dev project does

I have built an Electron app and I'm trying to build it out. It works perfectly when I run it in the dev environment with "npm start", but once I build it out, it fails. I've tried this with electron forge and electron packager. Essentially, my app lives in the task tray and scans a folder every second. If the folder is not empty, it shows the window. While the window is shown, the loop stops. Once the main.js file receives a command that the user actions were completed, it hides the window and goes back to the tray to resume scanning. Here is my main.js file:
const { app, BrowserWindow, Tray, Menu } = require('electron')
var ipcMain = require('electron').ipcMain;
const Store = require('electron-store');
const store = new Store();
const fs = require('fs');
shouldScan = true;
// global window declaration function
var win;
async function createWindow () {
win = new BrowserWindow({
width: 500,
height: 250,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
}
})
win.setMenuBarVisibility(false)
win.loadFile('index.html')
tray = new Tray('spongebob.ico')
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show App', click: function () {
win.show()
}
},
{
label: 'Quit', click: function () {
app.isQuiting = true
app.quit()
}
}
])
tray.setToolTip('This is my application.')
tray.setContextMenu(contextMenu)
win.on('minimize', function (event) {
event.preventDefault()
shouldScan = true
scanning()
win.hide()
})
win.on('show', function (event) {
event.preventDefault()
shouldScan = false
})
await sleep(1000)
win.minimize()
}
// start the application
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
//allow delays
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//check the designated folder and stop the loop while showing the app window from the tray
async function scanning(){
while(shouldScan){
console.log('scanning')
if(store.get('default_path') != null){
files = fs.readdirSync(store.get('default_path'));
if(files.length > 0){
fs.rename(store.get('default_path') + "/" + files[0], store.get('default_path') + "/encounter", err => {
if (err) {
console.error(err)
return
}
})
console.log('should have shown')
win.show()
shouldScan = false
}
}
await sleep(1000)
}
}
//start the scanning funciton again when the signal is received
ipcMain.on('processInput', function(event, status) {
win.hide()
shouldScan = true
scanning()
});
The error I'm experiencing is that the window never goes to tray. Its even supposed to minimize 1 second after launching but it doesn't do that either. Actually, none of the scripts in the main file run other than creating the window. If the window is minimized and its target folder is not empty, it does not re-show the window. The dev tools within the window don't show any errors. I don't know how to have a command prompt running while the packaged .exe is running either, though. Does anyone have some advice?
for anyone in the future, it seems that electron does not like just local file paths. When I was creating the new Tray('spongebob.ico') that wasn't good. doing this seemed to fix the error:
new Tray(path.join(__dirname, 'asset','img', 'spongebob.png'));
obviously I had to create the correct path and file type.

How to use ipcRender inside executeJavascript?

I tried just simply putting the ipcRenderer message inside of executeJavascript but it returned
ipcRenderer is not defined
my ipcRender is defined using window.ipcRenderer:
const { ipcRenderer, remote } = require('electron');
window.ipcRenderer = ipcRenderer;
//and then
remote.getCurrentWebContents().executeJavaScript(`settingsDiv.addEventListener('click', function() { ipcRenderer.send('test','ayy'); } );`)
This is loaded as a preloaded script for a webpage.
There is no need to take that path on a preload.
Something like this should work instead:
const { ipcRenderer } = require('electron');
document.addEventListener('DOMContentLoaded', (event) => {
const settingsDiv = document.querySelector('<?>'); // replace <?> with your selector for that div element
settingsDiv.addEventListener('click', () => {
ipcRenderer.send('test', 'ayy');
});
}
(the preload runs first, then the page is rendered. So we have to wait until the DOM content is loaded and the div is available)

Write to file in "appData" in Electron. Where to add import { app } from "electron";?

I am making my first Electron application. I am trying to save a text file to the appData folder (example C:\Users\user\AppData\Roaming). I know I need to add import { app } from "electron"; some where but I am unsure where to place it.
In my index.js javascript I am writing the database settings that the user submits in his form to a text file. This is where I need to have the appData directory address.
// Write data to text file
var filepath = app.getPath("appData")
var filename = "database_quick_image_forensics.txt"
var inp_data = inp_host + "|" + inp_username + "|" + inp_password + "|" + inp_database_name + "|" + inp_table_prefix;
write_to_file(filepath, filename, inp_data);
My entire code is below:
./setup/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Setup</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="../_webdesign/dark/dark.css" />
<!-- // CSS -->
<!-- jQuery -->
<script>window.$ = window.jQuery = require('../javascripts/jquery/jquery-3.4.1.js');</script>
<script src="../javascripts/jquery/jquery-3.4.1.js" charset="utf-8"></script>
<!-- //jQuery -->
<!-- jQuery -->
<script src="./index.js" charset="utf-8"></script>
<!-- //jQuery -->
</head>
<body>
<div id="main_single_column">
<h1>Setup</h1>
<!-- Feedback -->
<div id="feedback_div" class="success">
<p id="feedback_p">Success</p>
</div>
<!-- //Feedback -->
<!-- Database connection form -->
<p>Host:<br />
<input type="text" name="inp_host" id="inp_host" value="localhost" />
</p>
<p>Port:<br />
<input type="text" name="inpport" id="inp_port" value="" />
</p>
<p>Username:<br />
<input type="text" name="inp_username" id="inp_username" value="root" />
</p>
<p>Password:<br />
<input type="text" name="inp_password" id="inp_password" />
</p>
<p>Database name:<br />
<input type="text" name="inp_database_name" id="inp_database_name" value="quick" />
</p>
<p>Table prefix:<br />
<input type="text" name="inp_table_prefix" id="inp_table_prefix" value="cf_" />
</p>
<p>
<button id="form_connect_to_database_submit">Connect to database</button>
</p>
<!-- //Database connection form -->
</div>
</body>
</html>
./setup/index.js
const fs = require('fs');
// Action = On submit
$(document).ready(function(){
$("#form_connect_to_database_submit").click( function() {
// Feedback
$('#feedback_div').show();
$('#feedback_div').removeClass("success");
$('#feedback_div').addClass("info");
$('#feedback_p').text("Connecting!")
// get all the inputs
var inp_host = $("#inp_host"). val();
var inp_username = $("#inp_username"). val();
var inp_password = $("#inp_password"). val();
var inp_database_name = $("#inp_database_name"). val();
var inp_table_prefix = $("#inp_table_prefix"). val();
// Test connection
var connection_result = connect_to_database(inp_host, inp_username, inp_password, inp_database_name, inp_table_prefix);
if(connection_result != "connection_ok"){
// Connection Failed
$('#feedback_div').removeClass("info");
$('#feedback_div').addClass("error");
$('#feedback_p').text(connection_result)
}
else{
// Connection OK
$('#feedback_div').removeClass("info");
$('#feedback_div').addClass("success");
$('#feedback_p').text("Connected")
// Write data to text file
var filepath = app.getPath("appData")
var filename = "database_quick_image_forensics.txt"
var inp_data = inp_host + "|" + inp_username + "|" + inp_password + "|" + inp_database_name + "|" + inp_table_prefix;
$('#feedback_p').text("Connected " + filepath)
write_to_file(filepath, filename, inp_data);
// Feedback
$('#feedback_div').removeClass("info");
$('#feedback_div').addClass("success");
$('#feedback_p').text("Connected to")
}
});
$('#inp_host').focus();
});
// Function connect to database
function connect_to_database(inp_host, inp_username, inp_password, inp_database_name, inp_table_prefix){
var mysql = require('mysql');
// Add the credentials to access your database
var connection = mysql.createConnection({
host : inp_host,
user : inp_username,
password : null, // or the original password : 'apaswword'
database : inp_database_name
});
// connect to mysql
connection.connect(function(err) {
// in case of error
if(err){
console.log(err.code);
console.log(err.fatal);
return err.code;
}
});
// Perform a query
$query = 'SELECT * FROM `cf_admin_liquidbase` LIMIT 10';
connection.query($query, function(err, rows, fields) {
if(err){
console.log("An error ocurred performing the query.");
console.log(err);
return;
}
console.log("Query succesfully executed", rows);
});
return "connection_ok";
} // connect_to_database
// Function write setup
function write_to_file(filepath, filename, inp_data){
var fullpath = filepath + "\\" + filename;
fs.writeFile(fullpath, inp_data, (err) => {
// throws an error, you could also catch it here
if (err) throw err;
// success case, the file was saved
console.log('Lyric saved!');
});
} // write_to_file
./main.js
const { app, 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 win
function createWindow () {
// Create the browser window.
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()
// Emitted when the window is closed.
win.on('closed', () => {
// 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.
win = 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', () => {
// 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 (win === null) {
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.
I know I need to add import { app } from "electron"; some where but I
am unsure where to place it.
The app module is always (in my experience) imported in your main process so you can control the applications lifecycle. However, if you want to use some of the app module functionality in your renderer process, you can import it there through the remote module ( as shown in the accepted answer to this question: How to use electron's app.getPath() to store data? )
const remote = require('electron').remote;
const app = remote.app;
console.log(app.getPath('userData'));
The main and renderer processes are key concepts in Electron so I'd suggest reading up on those. The gist is that you have one main process – it has no visual representation and it is involved with the lifecycle of your app, creating and destroying renderer processes (like BrowserWindows), communication between renderer processes, etc. – and you can have as many renderer processes as you need.
So if you want to read and write files you can do it in the renderer process as shown above – or you can do it in the main process. In the latter case, if a renderer process wants to save a file, it can message the main process through IPC, sending the data to be saved.
Which way you do it is an architectural choice.
To get the app path at your main process. Then use this code at your main.js
switch(process.platform) {
case 'darwin': {
return path.join(process.env.HOME, 'Library', 'Application Support', ...);
}
case 'win32': {
return path.join(process.env.APPDATA, ...);
}
case 'linux': {
return path.join(process.env.HOME, ...);
}
}
And going to get the path from the renderer then use this code at your renderer
const remote = require('electron').remote;
const app = remote.app;
console.log(app.getPath('userData'));
But to use require at your renderer, please make sure nodeintegration is true.
If I were you, I was going to get the app path at main process and store the file at main process as well.
Hence, importing many dependencies at renderer process is not a good choice.
The renderer process mainly takes care of showing your app in the Chromium browser.
So to make this operation at main process. Use this
at your main.js
const { ipcMain } = require('electron')
const appPath = () => {
switch(process.platform) {
case 'darwin': {
return path.join(process.env.HOME, 'Library', 'Application Support');
}
case 'win32': {
return process.env.APPDATA;
}
case 'linux': {
return process.env.HOME;
}
}
}
const writeToFile = (fileName, inData) => {
const fullPath = path.join(appPath(), "\\", fileName);
fs.writeFile(fullPath, inData, (err) => {
// throws an error, you could also catch it here
if (err) throw err;
// success case, the file was saved
console.log('Lyric saved!');
});
} // write_to_file
ipcMain.on('WRITE_TEXT', async (event, arg) => {
writeToFile(arg.fileName, arg.inData)
});
At your renderer process add this code.
const {ipcRenderer} = require('electron')
ipcRenderer.sendSync('WRITE_TEXT',{fileName, inData})
As you can see, at renderer process, this is sending the inp_data to your main process through 'WRITE_TEXT' IPC channel.
One more thing here, at your code. You are connecting your DB at your renderer and it's possible but this is not a right choice. Please think while you are having the several renderer. You should move this to main process too.

autoUpdater.setFeedURL is not a function

I'm attempting to implement windows auto update functionality in an electron app (which may lead to my early death) and I'm getting this error.
This is the URL I'm passing for testing purposes
EDIT: my electron app is using the two package.json structure and this code is in my app>main.js file
const feedURL = 'C:\\Users\\p00009970\\Desktop\\update_test';
autoUpdater.setFeedURL(feedURL);
autoUpdater.checkForUpdates();
EDIT2: Thanks to #JuanMa, I was able to get it working. Here is the code.
// auto update functionality
const {autoUpdater} = require('electron')
// local file system example: const feedURL = 'C:\\Users\\john\\Desktop\\updates_folder';
// network file system example: const feedURL = '\\\\serverName\\updates_folder';
const feedURL = '\\\\serverName\\updates_folder';
app.on('ready', () => {
autoUpdater.setFeedURL(feedURL);
// auto update event listeners, these are fired as a result of autoUpdater.checkForUpdates();
autoUpdater.addListener("update-available", function(event) {
});
autoUpdater.addListener("update-downloaded", function(event, releaseNotes, releaseName, releaseDate, updateURL) {
//TODO: finess this a tad, as is after a few seconds of launching the app it will close without warning
// and reopen with the update which could confuse the user and possibly cause loss of work
autoUpdater.quitAndInstall();
});
autoUpdater.addListener("error", function(error) {
});
autoUpdater.addListener("checking-for-update", function(event) {
});
autoUpdater.addListener("update-not-available", function(event) {
});
// tell squirrel to check for updates
autoUpdater.checkForUpdates();
})
Are you including the autoUpdater module correctly?
const {autoUpdater} = require('electron')
If so try to execute the code after the app 'ready' event.
app.on('ready', () => {
const feedURL = 'C:\\Users\\p00009970\\Desktop\\update_test';
autoUpdater.setFeedURL(feedURL);
autoUpdater.checkForUpdates();
})

Phonegap confirmation alert

I tried putting the following code into a html and ran it and I uploaded it in my server and opened the link in my Safari browser in my iPhone and the clicked on Show Confirm and no window popups up! Can someone please help.
<!DOCTYPE html>
<html>
<head>
<title>Notification Example</title>
<script type="text/javascript" charset="utf-8" src="http://mobile-web-development-with-phonegap.eclipselabs.org.codespot.com/svn-history/r99/trunk/com.mds.apg/resources/phonegap/js/phonegap-1.0.0.js"></script>
<script type="text/javascript" charset="utf-8">
// Wait for PhoneGap to load
document.addEventListener("deviceready", onDeviceReady, false);
// PhoneGap is ready
function onDeviceReady() {
// Empty
}
// process the confirmation dialog result
function onConfirm(button) {
alert('You selected button ' + button);
}
// Show a custom confirmation dialog
function showConfirm() {
navigator.notification.confirm(
'You are the winner!', // message
onConfirm, // callback to invoke with index of button pressed
'Game Over', // title
'Restart,Exit' // buttonLabels
);
}
</script>
</head>
<body>
<p>Show Confirm</p>
</body>
</html>
The code that you are using (navigator.notification.confirm) is specifically for the mobile platform that is it is meant to run within the PhoneGap mobile application. If you would like to test out the dialogs/confirm messages on a browser before compiling it into an application, I would suggest using a hybrid approach that detects the environment of the application and uses either the native confirm(message) or the PhoneGap specific Notification API. Below is an example Object that has been working for me:
/**
* The object encapsulates messaging functionality to work both in PhoneGap and
* browser environment.
* #author Zorayr Khalapyan
*
*/
var MessageDialogController = (function () {
var that = {};
/**
* Invokes the method 'fun' if it is a valid function. In case the function
* method is null, or undefined then the error will be silently ignored.
*
* #param fun the name of the function to be invoked.
* #param args the arguments to pass to the callback function.
*/
var invoke = function( fun, args ) {
if( fun && typeof fun === 'function' ) {
fun( args );
}
};
that.showMessage = function( message, callback, title, buttonName ) {
title = title || "DEFAULT_TITLE";
buttonName = buttonName || 'OK';
if( navigator.notification && navigator.notification.alert ) {
navigator.notification.alert(
message, // message
callback, // callback
title, // title
buttonName // buttonName
);
} else {
alert( message );
invoke( callback );
}
};
that.showConfirm = function( message, callback, buttonLabels, title ) {
//Set default values if not specified by the user.
buttonLabels = buttonLabels || 'OK,Cancel';
var buttonList = buttonLabels.split(',');
title = title || "DEFAULT TITLE";
//Use Cordova version of the confirm box if possible.
if (navigator.notification && navigator.notification.confirm) {
var _callback = function (index) {
if ( callback ) {
//The ordering of the buttons are different on iOS vs. Android.
if(navigator.userAgent.match(/(iPhone|iPod|iPad)/)) {
index = buttonList.length - index;
}
callback( index == 1 );
}
};
navigator.notification.confirm(
message, // message
_callback, // callback
title, // title
buttonLabels // buttonName
);
//Default to the usual JS confirm method.
} else {
invoke( callback, confirm( message ) );
}
};
return that;
})();
Hope this helps! Let me know if you have any questions.

Resources