I wanted to try out Electron JS and get started with the basics by creating a simple app that I'll be using myself on a daily basis. I chose to create a Mac menubar application to render devdocs.io
. I am a huge fan of devdocs.io
and use it frequently so I thought it will be super handy to have all the documentation right on the menu bar.
Since this is my first attempt at using Electron, this article will document the steps I took to develop the application.
Create project folder
mkdir menubar-dev-docs
cd menubar-dev-docs
Initialize npm package
npm init -y
Typescript configuration
npm install typescript --save-dev
tsc --init
creates a typescript config file tsconfig.json
.
Add electron as dev dependency
npm install electron --save-dev
Webpack setup
We will use webpack to bundle and build the application.
Install webpack related dev dependencies
npm install webpack webpack-cli ts-loader --save-dev
Create webpack.config.js
in the root folder and use the code below. We are specifying ./src/main.ts
as the entry point of our application. Webpack will build it and output a bundled minified version as main.js
inside /dist
folder
const path = require('path');
// Electron Webpack Configuration
const electronConfiguration = {
// Build Mode
mode: 'development',
// Electron Entrypoint
entry: './src/main.ts',
target: 'electron-main',
resolve: {
alias: {
['@']: path.resolve(__dirname, 'src'),
},
extensions: ['.tsx', '.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
include: /src/,
use: [{ loader: 'ts-loader' }],
},
],
},
output: {
path: __dirname + '/dist',
filename: 'main.js',
},
};
module.exports = [electronConfiguration];
Create main script file src/main.ts
The main.ts
is the main entry point to an Electron application. This file runs the electron main process which controls the lifecycle of the application, graphical user interface and the renderer processes. An Electron app can have only one main process but have multiple renderer processes.
import { app, BrowserWindow } from 'electron';
const createWindow = (): void => {
const mainWindow = new BrowserWindow({
width: 1020,
height: 800,
});
mainWindow.loadURL('https://devdocs.io');
};
// call createWindow method on ready event
app.on('ready', createWindow);
When app initializes, electron fires a ready
event. Once app is loaded createWindow
callback function is called. createWindow
creates a BrowserWindow
object with height
and width
property and loads devdocs.io
URL.
The BrowserWindow
object represents the Renderer process (web page). We can create multiple browser windows, where each window uses its own independent Renderer.
Start the application
At this point we should be able to start our application and see it running. In order to run the application we need to specify two scripts inside scripts section of package.json
"scripts": {
"compile": "webpack",
"start": "npm run compile && electron dist/main.js"
},
compile
script will trigger webpack to compile the application and output the bundled main.js
file inside the dist
folder. start
script will invoke the compile
script first and initiate electron to execute the build output file dist/main.js
Once we have these script setup, we can start the application by using npm start
We now have a screen rendering devdocs.io webpage. The ultimate goal however is to have it as a menu bar app.
Menubar Tray Object
Next step will be to create a mac menubar tray element and toggle the BrowserWindow
using the tray element.
Electron provides a Tray
class to add icons and context menu to the notification area of the menu bar.
Let's create a class called TrayGenerator
which takes in an object of BrowserWindow
and string path for the app icon and creates a Tray object. The browser window that was created previously in main.js
would be toggled using the Tray icon from the menubar.
import { app, Tray, BrowserWindow, nativeImage, Menu } from 'electron';
class TrayGenerator {
tray: Tray;
constructor(public mainWindow: BrowserWindow, public iconPath: string) {
this.createTray();
}
}
TrayGenerator
class has a public property called tray
to access to Tray
object. createTray()
method is called on the constructor when TrayGenerator
object is initialized. createTray()
method creates the Tray
object and toggles the browser window on click.
Add a private method createTray()
to the TrayGenerator
class
private createTray = () => {
this.tray = new Tray(this.createNativeImage());
this.tray.setIgnoreDoubleClickEvents(true);
this.tray.on('click', this.toggleWindow);
}
Tray
object requires a NativeImage
object during initialization. Add another private method createNativeImage()
to the TrayGenerator
class which creates an object of NativeImage
private createNativeImage() {
// Since we never know where the app is installed,
// we need to add the app base path to it.
let appPath = app.getAppPath();
appPath = appPath.endsWith('dist') ? appPath : `${appPath}/dist`
const path = `${appPath}/${this.iconPath}`;
const image = nativeImage.createFromPath(path);
// Marks the image as a template image.
image.setTemplateImage(true);
return image;
}
Finally we need add a method toggle window when the menubar Tray icon is clicked. Add two more private methods toggleWindow()
and showWindow()
to the TrayGenerator
class.
private toggleWindow = () => {
const isVisible = this.mainWindow.isVisible();
const isFocused = this.mainWindow.isFocused();
if (isVisible && isFocused){
this.mainWindow.hide();
} else if (isVisible && !isFocused){
this.mainWindow.show();
this.mainWindow.focus();
} else {
this.showWindow();
}
};
private showWindow = () => {
// set the position of the main browser window
this.mainWindow.setPosition(this.tray.getBounds().x, 0, false);
this.mainWindow.show();
this.mainWindow.setVisibleOnAllWorkspaces(true); // put the window on all screens
this.mainWindow.focus(); // focus the window up front on the active screen
this.mainWindow.setVisibleOnAllWorkspaces(false); // disable all screen behavior
};
TrayGenerator
class finally look like below:
import { app, Tray, BrowserWindow, nativeImage, Menu } from 'electron';
class TrayGenerator {
tray: Tray;
constructor(public mainWindow: BrowserWindow, public iconPath: string) {
this.createTray();
}
private createTray = () => {
this.tray = new Tray(this.createNativeImage());
this.tray.setIgnoreDoubleClickEvents(true);
this.tray.on('click', this.toggleWindow);
};
private createNativeImage() {
// Since we never know where the app is installed,
// we need to add the app base path to it.
// on dev env, the build app is dist, once packaged electron-builder package it as dist/assets, but app path is not in dist so append dist for pacaking
let appPath = app.getAppPath();
appPath = appPath.endsWith('dist') ? appPath : `${appPath}/dist`;
const path = `${appPath}/${this.iconPath}`;
const image = nativeImage.createFromPath(path);
// Marks the image as a template image.
image.setTemplateImage(true);
return image;
}
private toggleWindow = () => {
const isVisible = this.mainWindow.isVisible();
const isFocused = this.mainWindow.isFocused();
if (isVisible && isFocused) {
this.mainWindow.hide();
} else if (isVisible && !isFocused) {
this.mainWindow.show();
this.mainWindow.focus();
} else {
this.showWindow();
}
};
private showWindow = () => {
this.mainWindow.setPosition(this.tray.getBounds().x, 0, false);
this.mainWindow.show();
this.mainWindow.setVisibleOnAllWorkspaces(true); // put the window on all screens
this.mainWindow.focus(); // focus the window up front on the active screen
this.mainWindow.setVisibleOnAllWorkspaces(false); // disable all screen behavior
};
}
export default TrayGenerator;
Use TrayGenerator
to create Tray
object on the app ready
event specified in main.ts
// call createWindow method on ready event
app.on('ready', () => {
createWindow();
const trayGenerator: TrayGenerator = new TrayGenerator(
mainWindow,
'assets/IconTemplate.png'
);
tray = trayGenerator.tray;
});
Note that the mainWindow
object is created when we call the createWindow()
method and the mainWindow
is defined in the global scope. We moved the mainWindow
from the fuction scope to global so that the object is not lost from the memory during garbage collection.
The final main.ts
file:
import { app, BrowserWindow, Tray } from 'electron';
import TrayGenerator from './TrayGenerator';
// NOTE: declare mainWindow and tray as global variable
// tray will be created out of this mainWindow object
// declaring them inside createWindow will result in tray icon being lost because of garbage collection of mainWindow object
let mainWindow: BrowserWindow;
let tray: Tray;
const createWindow = (): void => {
mainWindow = new BrowserWindow({
width: 1020,
height: 800,
frame: false, // hide the app window frame
show: false, // do not load main window on app load
fullscreenable: false, // prevent full screen of main window
resizable: true, // allow resizing the main window
alwaysOnTop: false,
});
mainWindow.loadURL('https://devdocs.io');
};
// call createWindow method on ready event
app.on('ready', () => {
createWindow();
const trayGenerator: TrayGenerator = new TrayGenerator(
mainWindow,
'assets/IconTemplate.png'
);
tray = trayGenerator.tray;
});
This was a quick experiment to get started with the basics of Electron JS.
Links:
Github Repo: HERE
Download dmg file: HERE
Top comments (0)