Developer Programs

Learn

Docs

Wrapping Responsive UI Apps in Electron

Technical Info > Wrapping Responsive UI Apps in Electron

Responsive web applications are built using HTML, CSS, and JavaScript. While these technologies can produce beautiful user interfaces, their ability to interact with the user’s environment is sand boxed in order to limit the damage that can be done by potentially malicious code.

This means that web apps have less access to their environment than desktop apps, by design. While HTML 5 allows web apps to access some features of mobile devices, a web app couldn’t say, drive cash dispense hardware.

Here are a few things that desktop apps can do that traditional web apps cannot:

  • Work with the local file system
  • Access/update the Windows registry
  • Communicate with hardware devices
  • Launch other desktop apps
  • Display native OS dialogs, like the File Open dialog
  • Access system information like the OS, screen resolution, etc
  • Access user information like the home directory
  • Take screen shots

When a Jack Henry responsive web app must be able to do the same kinds of things that desktop apps can do, we wrap it with a native wrapper: a cross-platform native executable for Windows, Mac, or Linux that wraps your responsive web app with a native executable that acts like any other native app: you launch it, minimize it, maximize it, close it, and switch to other apps just like any other native app. It shows up in the Windows taskbar and the Mac dock like any other native app. That’s because it is a native app, one that wraps your responsive web app. And in addition to wrapping your web app with a native executable, the native wrapper also extends your responsive web app with access to all the same kinds of functionality that a traditional desktop app has.

We support Electron as an open-source native wrapper solution. Electron wraps your web app with a native wrapper for Windows, Mac and/or Linux, displaying your app within an embedded copy of the Chromium browser. It also extends your web app with the same kinds of functionality available to native apps. This document covers the basic setup for getting your responsive web app wrapped in Electron; see the Electron site for more documentation.

Reference the Electron sample app repo to see how to wrap a Responsive UI app with Electron.

Progressive Web Apps

Progressive web apps are another good option for wrapping your Responsive UI web application; however, we only focus on Electron in this document.

Also, Electron creates a native wrapper for desktop environments: Windows, Mac, and Linux. Check out the Cordova project if you need a native wrapper for mobile environments.

In order to wrap your web app and extend it with native capabilities, Electron uses two separate processes:

  • Main process: This is a non-visual Node.js-based process that (a) handles all the setup for wrapping your app and (b) provides native functionality that extends your app’s capabilities.
  • Renderer process: This is your existing responsive web app. This is the process that users see and interact with.

These processes use two-way communication to coordinate activities. The renderer process (your web app) typically initiates that communication, whenever it needs the main process to perform some native functionality on its behalf. Depending on what the renderer process requests, the main process may simply perform the requested activity (say launch Excel) or it may respond with some kind of results, like a listing of files in a particular directory. However, the main process can also asynchronously initiate communicate with the renderer process, typically to let it know that some native-level event has occurred.

Diagram of Electron processes

Wrapping your app in Electron

The Electron wrapper can either fully embed your Angular app within the wrapper and display the embedded app, or it can point to your app hosted on a web server.

The approach you choose here determines two things: how you provide ongoing updates to your application and how your renderer process communicates with your Electron main process.

Embedded app model

If you embed your Angular app within the Electron wrapper, it runs locally, fully hosted within the Electron wrapper. This model has one advantage and one major disadvantage.

  • The advantage is simpler communication between the main process and renderer process. Because everything is packaged locally, the two processes can communicate directly using the simple Electron IPC mechanism.
  • The downside is that, since the entire app is local, every update to the app requires you to update the app on the user’s workstation using an installer. This fact will make the embedded approach less attractive for most Jack Henry applications.
The embedded app model allows simple, node-based IPC, but every update requires you to reinstall the Electron wrapper on the user's desktopElectron wrapping an embedded web app

Hosted app model

The hosted app model, where the Electron wrapper points to your app hosted on a web server, has one major advantage and one disadvantage.

  • The advantage is significantly easier updates. When you need to update your app, you can (for the most part) simply redeploy the app on the web server, and all copies of the Electron wrapper installed on user workstations will display the updated version.

    • The only exception to this is when you add or update any of the electron functionality in the main process. Let’s say you add a new feature that launches Excel on the user’s workstation. Since that involves a change to the Electron wrapper itself (the new feature to launch Excel), you would need to update the Electron wrapper on each user’s workstation with an installer. Contrast this to the embedded app model, where every app update requires you to update the Electron wrapper on each user’s workstation. Fortunately, the majority of updates to your application won’t be updating the Electron wrapper, so installer updates will be the exception and not the rule.
  • The downside is that communication between the main process and the renderer process requires extra steps in order to avoid introducing potential security risks. The simple IPC communication mechanism in Electron is node-based. If your app is hosted on a server, you absolutely do not want to turn on node integration within the hosted renderer process (your web app) as this is a major security risk, so we have to set up a couple of extra layers to the communication in order to preserve security. We talk more about that below.

Despite the extra layers required for communication, we anticipate that most teams needing native wrappers will choose the hosted model in order to avoid having to redeploy the Electron wrapper on each workstation for every app update, so the rest of this document assumes that you’re using the hosted model.
The hosted app model involves more steps for communication between your app and Electron to ensure security, but allows you to provide most updates through simple deployment to the web serverElectron wrapping a hosted web app

Extending your app with Electron

We mentioned earlier that Electron extends your app’s potential by adding native app functionality, such as working with the local file system, communicating with hardware devices, launching other desktop apps, and much more. In Electron, the main process (electron.js) is where you add those kinds of functionality.

The Electron main process uses Node.js, which is a JavaScript runtime. The Node.js runtime out of the box includes most of the functionality your main process will need.

And if you need functionality beyond that, there are hundreds of thousands of additional modules (packages) that have been written for Node.js. You can explore all of this functionality at npm, a package manager for Node.js modules. npm helps you find, install, and manage the packages used by your Electron main process. In fact, Electron itself is installed with npm.

The electron.js file in the Electron sample app repo shows examples of using node functionality to extend the app with native functionality.

Any additional node modules that you reference from electron.js should be added to the main project’s package.json file and installed with npm, then they will be available for your electron.js file to use.

IMPORTANT

Node.js provides powerful native functionality. Node.js can potentially be used to harm the user’s workstation, network, and data.

Because of this, any time you add new functionality to your main process, ask yourself how a hacker might leverage it to harm the user. Keep this functionality as simple and benign as possible. NEVER code open-ended requests that might allow the renderer process to pass in unanticipated commands.

Secure communication between the main and renderer processes

As you look at Electron documentation and examples, you’ll see inter-process communication (IPC) used for the communication between the main process and the renderer process. The main process uses an ipcMain object to send and receive IPC messages, while the renderer process uses an ipcRenderer object to send and receive IPC messages. This communication can be synchronous for requests that can be fulfilled immediately or asynchronous for longer-running requests.

This IPC communication uses Node.js. In the embedded app model, it’s safe for both the main process and renderer process to use Node.js since all parts of the app are running local on the user’s workstation.

However, in the hosted app model (which most Jack Henry apps will likely use), your app is deployed to a web server. It is considered a security risk to enable node integration in a hosted web app due to the potential for cross-site scripting attacks against the user’s workstation. Enabling node integration in your web app would potentially allow a hacker to inject Node.js commands in your web app that could compromise the user’s workstation, network, and data. So, we must disable node integration in the renderer process for hosted apps.

But the Electron IPC mechanism itself is based on Node.js, so we’re unable to use that IPC mechanism directly from our renderer process.

Luckily there is a way around this. Electron can inject a preload script into your renderer process that acts as a communication bridge between your main and renderer processes (see the diagram above). This preload script is installed only on the user’s workstation, along with the Electron main script, so both are off limits to a hacker who compromises the web server. Your renderer process calls functions within the preload script, which in turn uses the Node-based IPC mechanism to pass the message to the main process, and vice versa. The worst a hacker can do in this scenario is modify your renderer process to call into the Node-based functionality that you’ve already defined, not add new, more potentially damaging Node-based functionality. Any time you add new native functionality to your main process, always ask yourself how a hacker might leverage it to harm the user.

We talk more below about setting up the communication bridge with the preload script.

Installing Electron

You must add the npm packages for both Electron and ngx-electron — an Angular wrapper that makes it simpler for your Angular app to use Electron — to your application’s package.json. Use npm install to install these packages for your solution.

Electron dependencies in package.json
"electron": "11.0.4",
"ngx-electron": "2.2.0",

Import NgxElectronModule into your app-module.

Import NgxElectronModule
// import into app.module
import { NgxElectronModule } from 'ngx-electron';

@NgModule({
    imports: [
        ...
        NgxElectronModule,
        ...
    ]
})

export class AppModule(){}

Setting Up the Electron main process

The Electron main process is simply a JavaScript file. We name this file electron.js in our examples, although it can have any name. (You’ll often see it named main.js in online examples.)

You’ll store all Electron-related files in the /src/electron folder in your solution, including electron.js, preload.js (the preload script for bridge communication), and the app’s icon. Start by copying this entire folder from the Electron sample app repo into your solution’s /src folder. It should contain electron.js, preload.js, icon.ico, and package.json files.

Let’s look at the electron.js file and talk about what it’s doing. Below is a listing of the entire file. As we look at the code, we see the following high-level things going on;

  • It imports the Node.js modules we need.
  • It defines a global JavaScript variable named win. We’ll assign the main browser window object to this variable in a moment, but for now, declaring the variable at this level makes it global so it won’t get garbage collected, which would close your Electron window.
  • It defines a function named createWindow() to set up and display your main Electron window. More on this later.
  • It creates an event handler for the app “ready” event that simply calls the createWindow() function to set up and display your main Electron window.
  • It creates event handlers for the app “window-all-closed” and “activate” events to set up proper window handling in MacOS.
  • It defines handlers for application-level IPC requests: getting the OS-level path separator character, getting the OS-defined home directory, getting the current Electron - version, displaying a progress value in the OS taskbar icon, and retrieving a list of files and folders within a specified directory.
Sample electron.js
// See this documentation for more details on using Electron to host Responsive UI web apps:
// https://jharesponsiveui.z19.web.core.windows.net/#/TechDocs/Electron

// Imports
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const { exec } = require('child_process');
const os = require('os');

// Keep a global reference to the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

// Create the browser window and load your web app
function createWindow() {

    // Create the browser window
    let newWindow = new BrowserWindow({

        // Don't show the window initially. We do that in the 'ready-to-show' 
        // event handler below.
        show: false,

        // Window size: should be large enough to comfortably display your app
        // without being larger than a typical user's monitor.
        width: 1400,
        height: 900,

        // Initial window background color; leave at this value for consistency
        // with other JHA apps.
        backgroundColor: '#333333',

        // Make the window resizable by the user.
        resizable: true,

        // The path to the icon file displayed for your app by the OS. This file
        // must be in ICO format and should be at least 256x256 for best results.
        icon: path.join(__dirname, 'icon.ico'),

        // IMPORTANT! Node integration MUST be turned off and a preload script used to
        // facilitate IPC *unless* your app will ALWAYS be fully embedded within Electron
        // and not hosted on a server, i.e you always specify the file:// protocol
        // for your web app and not  the http(s):// protocol. Enabling node integration
        // in a hosted web app is a serious XSS security risk.
        // https://electronjs.org/docs/tutorial/security#2-disable-nodejs-integration-for-remote-content
        webPreferences: {
            nodeIntegration: false,
            preload: path.join(__dirname, 'preload.js')
        }
    })

    // --------------------------------------------------------------
    // Load your web app into the Electron window.
    //
    // Your app can either be local (fully embedded with Electron)
    // and use the file:// protocol to access it, or it can be hosted
    // on a web server, in which case you use the http(s):// protocol 
    // to access it.
    // 
    // It can be useful to embed a local version of an Angular app 
    // while debugging it within Electron, but in production most
    // JHA products will host their app on a web server to make
    // updates easier. The only way to update a local app is with
    // full or partial installer updates. Updates can be made to a 
    // hosted app on the server as needed, without requiring updates
    // to your Electron deployment.
    //
    // Load your app using one of the following two options.
    // --------------------------------------------------------------

    // OPTION 1 (preferred for production): Most JHA products will
    // host their app on a web server since the update process is
    // much easier. Here's an example of loading a web app hosted
    // on a server. Comment this out if you use option 2.
    //newWindow.loadURL('https://my-server-based-application-url');

    // OPTION 2: Use the file:// protocol if your app will be fully
    // embedded within the Electron window and not hosted. This can
    // be useful when debugging an Angular app within Electron.
    // Here's an example of loading a web app locally. Comment this
    // out if you use option 1.
    newWindow.loadURL(`file://${__dirname}/index.html`);

    // --------------------------------------------------------------
    // Window management messages
    // --------------------------------------------------------------

    // Emitted when the app is ready to show. Do not change or remove!
    newWindow.once('ready-to-show', () => {

        // Maximize the window: this is optional and can be removed
        newWindow.maximize();

        // Show the window
        newWindow.show();
    })

    // Emitted when the window is closed. Do not change or remove!
    newWindow.on('closed', () => {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        newWindow = null
    })

    // Let the renderer process know when the app is trying to open an 
    // external link.  When it does, prevent the default behavior and
    // open the link in the users default browser. Do not change or remove!
    newWindow.webContents.on('new-window', function (e, url) {
        e.preventDefault();
        require('electron').shell.openExternal(url);
    })

    return newWindow;
}

// --------------------------------------------------------------
// Window management messages
// --------------------------------------------------------------

// This method will be called when Electron has finished initialization and
// is ready to create browser windows. Some APIs can only be used AFTER this
// event occurs. Do not change or remove!
app.on('ready', () => {
    win = createWindow();
})

// Quit when all windows are closed. Do not change or remove!
app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd+Q.
    if (process.platform !== 'darwin') {
        app.quit();
    }
})

// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', () => {
    if (win === null) {
        win = createWindow();
    }
})

// --------------------------------------------------------------
// Put your app's custom Electron functionality below here
// --------------------------------------------------------------

// Request path separator
ipcMain.on('req-path-separator', (event, arg) => {
    event.sender.send('res-path-separator', path.sep);
});

// Request for home directory
ipcMain.on('req-home-dir', (event, arg) => {
    event.sender.send('res-home-dir', os.homedir());
});

// Request for electron version
ipcMain.on('req-electron-version', (event, arg) => {
    // process.versions.electron contains the Electron version
    event.sender.send('res-electron-version', process.versions.electron);
});

// Request to set the taskbar progress indicator to the specified value
ipcMain.on('req-set-taskbar-progress', (event, arg) => {
    // Set the progress indicator in the taskbar icon
    win.setProgressBar(arg)
});

// Request for local files
ipcMain.on('req-local-files', (event, arg) => {
    const fs = require('fs');
    const path = require("path");
    var fileArray = [];
    var dir = arg + path.sep;

    // Gather a list of files and folders in the specified folder
    fs.readdir(dir, function (err, items) {

        if (items && items.length > 0) {
            for (var i = 0; i < items.length; i++) {
                var file = items[i];
                var type = "file";

                try {
                    if (fs.statSync(path.join(dir, file)).isDirectory())
                        type = "dir";
                }
                catch (err) {
                }

                fileArray.push({ "name": file, "type": type });
            }
            fs.stat(file, file_callback(fileArray));
        } else {
            fileArray.push({ "name": "This folder is empty.", "type": "empty" });
            fs.stat("This folder is empty.", file_callback(fileArray));
        }
    });

    // Send the result in a response message
    function file_callback(fileArray) {
        return function (err, stats) {
            event.sender.send('res-local-files', fileArray);
        }
    };
});

Let’s take a closer look at the createWindow() function.

  • It instantiates a new BrowserWindow object, passing in a JSON object that contains configuration parameters. BrowserWindow is the class for your Electron window. The configuration parameters include:
    • show specifies whether the window shows immediately. We set this to false here because we show the window during the “ready-to-show” event, providing a smoother start-up experience for the user.
    • width and height specify the size of the window. You have to hard code specific values here. You’ll want to ensure that the window size is large enough to comfortably display your app without being larger than a typical user’s monitor.
    • backgroundColor specifies the color displayed as the window background while your app loads. Leave this at the default value for consistency with other Jack Henry applications. Once your app loads, Electron will use the themed body background color as the window background color. It only uses the value specified here while your app loads.
    • resizable specifies whether your window is resizable. Leave this set to true since our application windows are resizable.
    • icon specifies which ICO file to use for your Electron wrapper. This file must be in ICO format and should be at least 256x256 for best results. You can use this same icon file for both Electron and the Electron packager/installer you create for your app.
    • The webPreferences section is critical if your app will be hosted on a web server and not fully embedded within the Electron wrapper. (See the “Wrapping your App in Electron” section above.) If you’re fully embedding your web app within the Electron wrapper, you should omit the webPreferences section. Otherwise you must include the webPreferences section and set the following values:
      • nodeIntegration must be set to false. Leaving node integration on within a hosted web app is a major security risk!
      • preload is the name of the preload script that Electron will load into the web app’s DOM. It contains the communication bridge. See the “Setting Up the Preload Script” section below for details.
  • After the BrowserWindow constructor, the code calls loadURL. Provide the path to your web application. Electron will load this into its embedded Chromium browser.
    • If your Electron wrapper points to a hosted web app (which should be the case for the majority of apps), simply provide the https:// path to the app.
    • If your Electron wrapper fully embeds your web app, provide a file:// path to your app’s index.html. Note that the index.html for this case must specify for the app to be able to properly load all of its JavaScript and CSS bundles.
    • Note that even if your Electron wrapper will point to a hosted web app in production, it can be handy to temporarily embed it in the Electron wrapper while you’re working on Electron integration. That allows you to build/run/test everything locally while developing, then switch it to point to the hosted production app when you’re ready.
  • The createWindow() function sets up event handlers for “ready-to-show”, “closed”, and “new-window” events. Leave this all as it is in the file since these are critical to proper window management.

Setting up the preload script

As we discussed earlier, hosted web apps cannot have node integration turned on for security reasons. This means that your renderer process cannot use Electron IPC to directly to communicate with the main process.

Instead, we define a preload script (preload.js) that proxies the communication between the renderer and main processes. This script file only exists on the user’s workstation, not on the web server, so a hacker that compromises the web server cannot change either the preload script or electron.js. Electron injects the preload script before loading your web app, so your app has access to the functionality in the preload script. So the only way a hacker can leverage Node.js functionality would be to make your app call into the existing Electron functionality defined within electron.js and preload.js, minimizing the potential for damage.

Most of the IPC communication between the renderer and main processes is initiated by the renderer process, typically in response to an action taken by the user. For example, say the user presses a button in your UI that is supposed to launch Excel; the renderer process (your web app) calls a function within preload.js, which in turn sends an IPC message to the main process. The main process receives the IPC message and uses Node.js functionality to launch Excel.

Let’s look at what’s happening in preload.js:

  • The first thing it does is define a NativeWrapperBridge object in the window object. Remember that preload.js is loaded into memory before your web app, so your web app can interact with this NativeWrapperBridge object in the DOM window object. The NativeWrapperBridge object contains pointers to each of its functions that the renderer process can call in order to initiate communication to the main process. You’ll have one entry in this object for every function that the renderer process will need to call.
  • It then implements a function for each type of communication that will be initiated from the renderer process. You’ll notice that the simplest versions of these like setZoomLevel() and setTaskbarProgress() simply send a synchronous IPC message to the main process since they don’t need anything in return. They essentially “throw a message over the wall” for the main process to fulfill. In other cases, like getElectronVersion() and localFiles(), it looks for a response after sending the request. You can also pass values to the IPC requests, as seen in the setTaskbarProgress() and localFiles() functions. const { ipcRenderer, webFrame } = require(’electron’);
Sample preload.js
console.log('Initializing native wrapper bridge');

window.NativeWrapperBridge = {
    setZoomLevel: setZoomLevel,
    getPathSeparator: getPathSeparator,
    getHomeDir: getHomeDir,
    getElectronVersion: getElectronVersion,
    setTaskbarProgress: setTaskbarProgress,
    localFiles: localFiles
};

function setZoomLevel(zoomLevel) {
    webFrame.setZoomFactor(zoomLevel / 100);
}

function getPathSeparator() {
    return new Promise(resolve => {
        ipcRenderer.send('req-path-separator');
        ipcRenderer.on('res-path-separator', (event, args) => {
            resolve(args);
        });
    });
}

function getHomeDir() {
    return new Promise(resolve => {
        ipcRenderer.send('req-home-dir');
        ipcRenderer.on('res-home-dir', (event, args) => {
            resolve(args);
        });
    });
}

function getElectronVersion() {
    return new Promise(resolve => {
        ipcRenderer.send('req-electron-version');
        ipcRenderer.on('res-electron-version', (event, args) => {
            resolve(args);
        });
    });
}

function setTaskbarProgress(value) {
    ipcRenderer.send('req-set-taskbar-progress', value);
}

function localFiles(path) {
    return new Promise(resolve => {
        ipcRenderer.send('req-local-files', path);
        ipcRenderer.on('res-local-files', (event, args) => {
            resolve(args);
        });
    });
}

Setting up a service that wraps the preload script

The preload script (preload.js) is a plain JavaScript file. Because calling plain JavaScript from TypeScript can be awkward, it helps to wrap the functionality exposed in preload.js with an Angular service.

Check out the AppNativeWrapperService displayed below. You can see that it defines a TypeScript-based service call for every function defined in window.NativeWrapperBridge for the renderer process to call. It first verifies that the NativeWrapperBridge is defined in window.

Your Angular web app could technically make all of these calls directly. You don’t have to create an Angular service to wrap the functionality in preload.js. But doing so encapsulates the awkward TypeScript/JavaScript boundary within this service, making it much simpler and easier to call into this functionality from the rest of your Angular code. Remember to import the service in app.module and include it in the providers section so it’s available globally throughout your application.

Sample service
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class AppNativeWrapperService {
    constructor() { }

    public enabled(): boolean {
        return 'NativeWrapperBridge' in window;
    }

    public setZoomLevel(value) {
        if (this.enabled()) {
            return (window as any).NativeWrapperBridge.setZoomLevel(value);
        }
    }

    public getPathSeparator(): Promise<string> {
        if (!this.enabled()) {
            return new Promise((resolve, reject) => {
                reject();
            });
        }

        return (window as any).NativeWrapperBridge.getPathSeparator();
    }

    public getHomeDir(): Promise<string> {
        if (!this.enabled()) {
            return new Promise((resolve, reject) => {
                reject();
            });
        }

        return (window as any).NativeWrapperBridge.getHomeDir();
    }

    public getElectronVersion(): Promise<string> {
        if (!this.enabled()) {
            return new Promise((resolve, reject) => {
                reject();
            });
        }

        return (window as any).NativeWrapperBridge.getElectronVersion();
    }

    public launchCalculator() {
        if (this.enabled()) {
            return (window as any).NativeWrapperBridge.launchCalculator();
        }
    }

    public setTaskbarProgress(value: number) {
        if (this.enabled()) {
            return (window as any).NativeWrapperBridge.setTaskbarProgress(value);
        }
    }

    public localFiles(filePath: string): Promise<string> {
        if (!this.enabled()) {
            return new Promise((resolve, reject) => {
                reject();
            });
        }

        return (window as any).NativeWrapperBridge.localFiles(filePath);
    }
}

Setting Up Electron scripts in package.json

Three things need to happen before running your app in Electron:

  • You have to build your app. If you’re using the embedded model, the build process needs to set the base href to “./”.
  • The contents of the electron folder need to be copied down into the dist folder so they’re with the rest of your compiled app.
  • You have to shell execute this command to display your Electron-wrapped app: electron dist/electron.js

It helps to define scripts within package.json for each of these steps. Here are some sample scripts that you can add to the scripts section in package.json:

  • "build-electron": "ng build --prod --base-href \"./\" && copy src\\electron\\* dist"
    • Usage: npm run build-electron
    • This script builds your app, setting base href to “./” (something you only need to do for the embedded model), then copies the contents of the electron folder down into the dist folder.
    • IMPORTANT: If you’re using the hosted model, omit the --base-href flag and just use "build-electron": "ng build --prod && copy src\\electron\\* dist"
    • It can be handy to have two build-electron scripts, one that sets base href for quick local embedded testing, and a production build-electron script that omits the --base-href flag for the hosted model.
  • "electron": "npm run build-electron && electron dist/electron.js"
    • Usage: npm run electron
    • This script runs the build-electron script above to build the app and copy files, then starts electron with the dist/electron.js file.
  • "test-electron": "copy src\\electron\\* dist && electron dist/electron.js"
    • Usage: npm run test-electron
    • This script simply copies the electron files down into dist and starts electron with the dist/electron.js file. This is handy when you want to quickly re-test your Electron-wrapped app without rebuilding it.

You can see examples of these kinds of scripts in the package.json file in the Electron sample app repo. Note that the copy commands shown above will work in Windows but not in UNIX-based operating systems like Mac and Linux.

While you the developer can run your Electron-wrapped app with electron dist/electron.js, that wouldn’t be an appropriate way for your users to run it. We talk about creating an installer for your Electron-wrapped app below.

Setting up your app header for Electron

Jack Henry responsive apps wrapped in Electron display a back button in the application header since they are not displayed within the browser and don’t have access to a browser back button. This section walks you through how to do that.

main-header.component.html

First, modify the jha-header-left element to display the back button when your app runs within a native wrapper. You do that by binding the jhaDisplayBackButton property to a boolean model value that you’ll set to true when running within Electron. You’ll also define a click event handler for the jhaBackButtonClicked event.

<jha-header-left [jhaDisplayBackButton]="isNativeWrapper" (jhaBackButtonClicked)="backButtonClicked()">

main-header.component.ts

Import ElectronService:

import { ElectronService } from 'ngx-electron';

Add a boolean property that specifies whether the app is in Electron:

public isNativeWrapper: boolean = false;

In the constructor, inject the Electron service and set isNativeWrapper to the isElectronApp property in this.electronService, which specifies whether your app is running within Electron.

constructor(private electronService: ElectronService) {
    this.isNativeWrapper = this.electronService.isElectronApp;
}

Add an event handler for the jhaBackButtonClicked event that just goes back one step in history.

public backButtonClicked() {
    window.history.back();
}

Creating an installer for your Electron native wrapper

While you as a developer can run your Electron-wrapped app by typing electron dist/electron.js from a shell prompt, that obviously isn’t the way you want your users to run your app.

There are two main utilities for packaging your Electron-wrapped app and creating an installer for end users to run: electron-builder and electron-packager.

You can use either of these utilities to package your app and generate an installer. We found electron-builder to be a little simpler, so the Electron sample app repo and this documentation describes how to use that, although electron-packager may be a better fit for your needs. The BranchAnywhere product uses electron-packager because of its support for distribution through the Microsoft Store.

Setting up electron-builder is relatively straightforward:

  • Add the sections highlighted in the code sample below to your package.json file.
  • Do an npm install from the shell to install electron-builder as a dev dependency.
  • Run npm run build-package from the shell to build your app for Electron then package it or run npm run package from the shell to simply package your app without rebuilding it.
  • electron-builder creates a packaged folder in your solution. This folder contains a setup EXE that anyone can run to install your app.
  • The packaged folder also contains a win-unpacked subfolder that contains all files for your application, including the EXE. If you distribute this manually instead of an installer, you must distribute all files and subfolders within the win-unpacked folder for your app to run.
You may also want to code sign your installer to avoid your users seeing an unrecognized app warning. See https://www.electron.build/code-signing for details.
{
    "name": "my-app",
    . . .
    "main": "./dist/electron.js",
    "scripts": {
        . . .
        "build-package": "npm run build-electron && electron-builder",
        "package": "electron-builder"
    },
    "build": {
        "appId": "<arbitrary, unique ID for your solution>",
        "win": {
            "target": "NSIS",
            "icon": "./dist/icon.ico"
        },
        "nsis": {
            "allowElevation": false,
            "allowToChangeInstallationDirectory": true,
            "oneClick": false,
            "perMachine": true
        },
        "electronDownload": {
            "cache": "C:\<path to your solution>\src\electron\cache"
        },
        "files": [
            "!**/node_modules/*"
        ],
        "directories": {
            "output": "packaged"
        }
    },
    "private": true,
    "dependencies": {
        . . .
    },
    "devDependencies": {
        . . .
        "electron-builder": "20.29.0",
        . . .
    }
}
Note that Electron embeds the Chromium browser and the Node.js runtime within your wrapped app, so the packaged version of your app may be larger than you expect.

Support options
Have questions on this topic?
Join the Responsive UI team in Microsoft Teams to connect with the community.
See something in this page that needs to change?
Send us feedback on this page.
Last updated Tue Jun 6 2023