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)