DEV Community

99darshan
99darshan

Posted on

Trying Out Electron JS

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];
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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
  };
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

This was a quick experiment to get started with the basics of Electron JS.

Links:

Github Repo: HERE

Download dmg file: HERE

Discussion (0)