Electron - How to load/update the current browser window with a new html file?

 A couple of days ago my superior of my current job told me to create a cross-platform desktop application with certain requirements. After some digging, I came to know about electron js which is currently the most popular javascript framework to create a cross-platform desktop app with HTML, CSS, and Javascript.

At first, I was thinking to start the development process with the help of some front-end framework like Vue or angular along with electron. But after some time I made a decision to dig deep into electron first without getting my hand dirty with those front-end frameworks.  Oh boy, that was quite a decision. 

After some quick digging in official electron docs, I faced a problem of changing the entire view aka changing the source Html file of the current browser window. It's easy a create a new browser window instance but changing the source file of the current browser window at runtime is a little bit complicated but not that much. I googled a lot to find a solution but didn't find any suitable one. After some trial and error, I came up with a solution to the problem just described. I am going to share the solution here for future reference. I think this solution will also help people who are faced with a similar problem.

Now, without boring you with useless chit-chat, let's dive into the solution. For this purpose, we will create a brand new project from scratch. For that open your terminal and enter the following commands.

npm init -y
npm install --save-dev electron

In the first line, we are telling our terminal to initialize the project and create a package.json file. We don't want to answer all questions, that's why we appended -y tag.  After that, we are installing the electron framework as a dev dependency in our project. 

Now, open the package.json file add the "main" and "start" property like the following. 

"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start":"electron ."
},

Here we are declaring the main.js file as the starting js file and in the start field we are declaring the command, we want to run when we enter npm start command. 

After that create a folder named src and inside that folder create four files like below. Two of them are HTML files and two of them are javascript files.  We named these index.html, index.js, anotherWindow.html, and anotherWindow.js. (see the picture below)

Now open main.js file and paste the following code. 

const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');

function createWindow () {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
//load the index.html of the app.
win.loadURL(url.format({
pathname : path.join(__dirname,'src/index.html'),
protocol:'file',
slashes:true
}))

}
//call the creation function when app is done loading
app.whenReady().then(createWindow)

//this event is invoked when user is quitting the application
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
//this event is used re-create window when user minimize the window and
//maximize again
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})

In the above code snippet, we are getting the app and BrowserWindow object from the electron module. The app object is responsible for controlling the overall application lifecycle. BrowserWindow object is responsible for creating the application window in which we will be putting stuff shortly. Inside createWindow function, we are creating a new instance of BrowserWindow with its width and height. We are also setting nodeIntegration property to true so that we can write node-specific code in our HTML file. When the application has finished start-up and done with the initialization app.whenReady promise will be called. That's why we added our window creation function call into the response of this promise call. The other two application events for which our code is being registered to handle are "window-all-closed" and "activate". The first one is invoked when the user tries to quit the application and later is invoked when the user maximizes the window.

Now open the index.html file inside the src folder and paste the following code. 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
<p>This is a test for multiple window</p>
<button id="changebtn"> change html </button>
<script>require('./index')</script>
</body>
</html>

In the above code snippet is a very basic HTML code template which contains nothing but some text and a button. 

Now open index.js file inside the src folder and paste the following code. 

const ipcRenderer = require('electron').ipcRenderer;

const changeButton = document.getElementById('changebtn');

changeButton.addEventListener('click',()=>{
ipcRenderer.send('change-view');
});

Here we are using the ipcRenderer object from the electron module to fire a custom event upon the button click. First, we are getting the reference of the button and then assigning a click event listener to it. Inside the event listener, we are firing an event with the help of the ipcRenderer object by calling ipcRenderer.send method. We will write code to subscribe to this event in the main.js file which is basically the main process. We can name this event anything we want but we must check for any typo when we are subscribing to this same event, otherwise, we will get an error. Here we named the event "change-view". 

Now let's run the project and check what we achieved so far. Run the project by entering the "npm start" command in the terminal from the project root folder. We will see similar output like the following image. At present when we click the button it doesn't do anything. You could ask, why is that? Because we are just invoking an event inside the event listener of this button but we haven't write any code to subscribe to this event and do something.    

We will render this new HTML file upon the button click of index.html which in our case is anotherWindow.html that's being created earlier. Now open the anotherWindow.html file and paste the following code.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h2>Hoorray , this is another window which has no button</h2>
<button id="backBtn">Back to previous screen</button>
<script>require('./anotherWindow')</script>
</body>
</html>

The above code snippet is also very basic html template just for demonstration purpose. In this code we created one h2 tag and also a button. We will render previous HTML page upon this button click. Also we included the script(anotherWindow.js) before closing the body tag. 

Now open the main.js file to include the code for the particular event subcription which we invoked earlier. paste the following code in main.js for the purpose of event subscription.

ipcMain.on('change-view', ()=>{
BrowserWindow.getAllWindows()[0].loadURL(url.format({
pathname : path.join(__dirname,'src/anotherWindow.html'),
protocol:'file',
slashes:true
}));
});

Here in main.js, we are using the ipcMain object from the electron module to subscribe to any event that is being sent to the main process from any renderer process. We are using ipcMain.send method for this purpose. Inside the callback function of this event subscription, we are telling BrowserWindow to load another HTML in its current view. Here BrowserWindow.getAllWindows() function will get all opened windows since it is possible to open multiple windows from the main process. We are getting the first window indexed by 0 as this is the main window created by the main process. Now run the project by entering the "npm start" command in your terminal. You can see the effect in the following video. 

So we have achieved what we are trying to do this far. We can now change the source HTML file inside the electron's main process. We had done this by invoking an event from the renderer process and subscribing to that event in the main process and loading a new HTML file as the source HTML file when the event is being invoked upon a button click.

Let's extend our example a little bit further. Currently, we can go to a new HTML page upon a button click but we can not go back to our previous HTML page. Let's invoke a new event for the main process to subscribe and take action.

Now, open the anotherWindow.js file which we created alongside anotherWindow.html. Paste the following code to that file.

const ipcRenderer = require('electron').ipcRenderer;
const backButton = document.getElementById('backBtn');

backButton.addEventListener('click',()=>{
ipcRenderer.send('back-to-previous');
});

In the above code snippet, we are getting the button reference we had created in anotherWindow.html page. In the event listener of that button, we are invoking a brand new event and we named it "back-to-previous". Now open main.js and add the subscription code to this event (see following code). 

ipcMain.on('back-to-previous',()=>{
BrowserWindow.getAllWindows()[0].loadURL(url.format({
pathname : path.join(__dirname,'src/index.html'),
protocol:'file',
slashes:true
}));
})

We can see that the above code snippet is similar to the one we wrote earlier. Just the event name and destination HTML file is changed. Now run the project by entering the "npm start" command in your terminal. We can now go back and forth between two HTML pages by changing the source HTML file inside the BrowserWindow and that too without creating any new window. 


The development of any desktop application using javascript's electron framework is best suited if we build a single page application. There are many SPA frameworks available these days. It is not recommended to create and use multiple HTML pages with the electron framework. In this post, I just played with the concept of pub-sub architecture that comes built-in with the Electron framework. Also, I didn't find any suitable resource where the above-described process is mentioned. That's why I decided to write down the process involved for my future reference. Hopefully, this can help people who are trying to figure out this same concept on their own. If you find this post helpful, do share it with your friends and colleagues. Thank you for reading this post with patience. 



Comments

Popular posts from this blog

Difference between __dirname vs __filename vs process.cwd() : once and for all

How to build an apk with React Native