1

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!

midnight-coding
  • 2,857
  • 2
  • 17
  • 27
MatCode
  • 114
  • 4
  • 14
  • Your preload script is wrong. In order to use the Context Bridge API the `contextIsolation` setting must be set to true (which is the default). This means that both your preload and renderer have their own context (i.e. their own window/document objects so all your query selectors won't work). Also the Context Bridge API is meant to expose an API for your renderer to consume. See this https://stackoverflow.com/q/69605882/1244884 – customcommander Jun 17 '22 at 07:05
  • Thank you, I'll keep this in mind the next time I set up such an app again. I enabled nodeIntegration in the meantime – MatCode Jun 17 '22 at 07:11

3 Answers3

0

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

terinao
  • 491
  • 4
  • 15
0

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));

};



});


MatCode
  • 114
  • 4
  • 14
  • I am still struggling with this, can't seem to find a way to get the data over to renderer, appreciate any guidance here! Right now I'm thinking of starting from scratch, this time round modularise things more, create another renderer for index.html, and then pass data from renderer_html to preload to main, and then main to preload to renderer_details – MatCode May 25 '22 at 09:07
0

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>
midnight-coding
  • 2,857
  • 2
  • 17
  • 27