Trying Out Electron JS
Tags: javascript, typescript
Published: 2021-01-20 16:01:27

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

```javascript
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.

```typescript
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`

```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.

```typescript
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

```typescript
  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`

```typescript
  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.

```typescript
  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:

```typescript
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`

```typescript
// 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:

```typescript
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](https://github.com/99darshan/menubar-dev-docs)

Download dmg file: [HERE](https://darshan-resume.s3.amazonaws.com/menubar-dev-docs-1.0.0.dmg)