Creating Desktop Applications With AngularJS and GitHub Electron

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

GitHub’s Electron framework (formerly known as Atom Shell) lets you write cross platform desktop application using HTML, CSS and JavaScript. It’s a variant of io.js run-time which is focused on desktop applications instead of web servers.

Electron’s rich native APIs enables us to access native things directly from our pages with JavaScript.

This tutorial shows us how to build a desktop application with Angular and Electron. The steps for this tutorial are as follows:

  1. Create a simple Electron application
  2. Use Visual Studio Code Editor to manage our project and tasks
  3. Integrate an Angular Customer Manager App with Electron
  4. Use Gulp tasks to build our application and create installers

Creating Your Electron Application

To get started, install Node if you don’t have it in your system already. Our application should be structured as follows:

project-structure

There are two package.json files in this project.

  • For development

    The package.json directly inside the project root contains the configurations, dependiencies for your development environment and build scripts. These dependencies and package.json file will not be included inside the production build.

  • For your application

    The package.json inside app folder is the manifest file for your application. So whenever you need to install npm dependencies to be used in your application directly, you should install it against this package.json

The format of package.json is exactly same as that of Node’s module. Your application’s startup script should be specified in main property inside your app/package.json.

app/package.json might look like this:

 
{ 
    name: "AngularElectron", 
    version: "0.0.0", 
    main: "main.js" 
} 

You can create both package.json files either by entering the npm init command. You can also manually create these files. Install npm dependencies that are required for packaging the application by entering following command in your command line prompt:


npm install --save-dev electron-prebuilt fs-jetpack asar rcedit Q

Creating your Startup Script

app/main.js is the entry point of our application. This script is responsible for creating the main window and handling the system events. main.js should look like the following:

 // app/main.js

// Module to control application life.
var app = require('app'); 

// Module to create native browser window.
var BrowserWindow = require('browser-window');
var mainWindow = null;

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function () {

  // Create the browser window.
  mainWindow = new BrowserWindow({ width: 800, height: 600 });

  // and load the index.html of the app.
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // Open the devtools.
  // mainWindow.openDevTools();
  // Emitted when the window is closed.
  mainWindow.on('closed', function () {

    // 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.
    mainWindow = null;
  });

});

Native access from the DOM

As I mentioned above, Electron enables you to access local npm modules and native APIs directly from your web pages. Create your app/index.html file as follows:

 
<html>
<body> 
  <h1>Hello World!</h1>
   We are using Electron 
   <script> document.write(process.versions['electron']) </script> on 
   <script> document.write(process.platform) </script>
   <script type="text/javascript"> 
      var fs = require('fs');
      var file = fs.readFileSync('app/package.json'); 
      document.write(file); 
   </script>

</body> 
</html> 

app/index.html is a simple HTML page. Here it reads your package.json using Node’s fs (file system) module and writes the content into the document body.

Run the Application

Once you have created the project structure, app/index.html, app/main.js, app/package.json , you’ll probably want to try running your initial Electron application to test it and make sure it’s working as expected.

If you’ve installed electron-prebuilt globally in your system, you can run our application with following command:


electron app 

Here electron is the command to run electron shell and app is our application folder name. If you don’t want to install Electron into your global npm modules, then run with your local modules installed into npm_modules folder as follows in your command line prompt:


"node_modules/.bin/electron" "./app" 

While you could do it that way, I recommend you create a gulp task to run your application in your gulpfile.js, so that you can integrate your task into Visual Studio Code Editor which we will check in next section.

 
// get the dependencies
var gulp        = require('gulp'), 
  childProcess  = require('child_process'), 
  electron      = require('electron-prebuilt');

// create the gulp task
gulp.task('run', function () { 
  childProcess.spawn(electron, ['./app'], { stdio: 'inherit' }); 
});

Run your gulp task: gulp run Our application might looks like this:

electron-app

Configuring Development Environment With Visual Studio Code

Visual Studio Code is a cross platform code editor from Microsoft. Behind the scene of VS Code is Electron and Microsoft’s proprietary monaco code editor. You can download Visual Studio Code here.

Open your electron application in VS Code.

open-application

Configure Visual Studio Code Task Runner

Lots of tools exist to automate our tasks like building, packaging or testing our application. Mostly we run these tools from the command line. VS Code’s built in task runner enables you to integrate your custom tasks into your project. You can run your grunt, gulp, MsBuild or any other tasks directly from within your project without moving to the command line.

VS Code can detect your grunt and gulp tasks automaticaly. Press ctrl + shift + p then type Run Task followed by Enter.

run-task

You will get all available tasks from your gulpfile.js or gruntfile.js.

Note that, you should have your gulpfile.js in root directory of your application.

run-task-gulp

ctrl + shift + b will execute build task from your task runner. You can override this intergration using the task.json file. Press ctrl + shift + p then type Configure Task followed by Enter. This will create .setting folder and a task.json in your project . You need to configure the tasks in tasks.json if you want to do more than simply run the task. For example you might want to run the application when you press the Ctrl + Shift + B . To do this edit the task.json file as follows:

 
{ 
  "version": "0.1.0", 
  "command": "gulp", 
  "isShellCommand": true, 
  "args": [ "--no-color" ], 
  "tasks": [ 
    { 
      "taskName": "run", 
      "args": [], 
      "isBuildCommand": true 
    } 
  ] 
} 

Root section says that the command is gulp. You can have more tasks inside tasks section as you want. Setting isBuildCommand true for a task will bind the Ctrl + Shift + B to that task. Currently VS Code supports one top level task only.

Now if you press Ctrl + Shift + B, gulp run will be executed.

You can read more about visual studio code tasks here

Debugging Electron Application

Open the debug panel and click configure button which will create a launch.json file inside .settings folder with debug configuration.

debug

We don’t need launch app.js configuration, so remove it.

Now your launch.json should be as follows:

 
{ 
  "version": "0.1.0", 
  // List of configurations. Add new configurations or edit existing ones. 
  // ONLY "node" and "mono" are supported, change "type" to switch. 
  "configurations": [
    { 
      "name": "Attach", 
      "type": "node", 
      // TCP/IP address. Default is "localhost". 
      "address": "localhost", 
      // Port to attach to.
      "port": 5858, 
      "sourceMaps": false 
     } 
   ] 
}

Change your gulp run task that we created before as follows, so that our electron will start in debug mode and will listen to port 5858:

 
gulp.task('run', function () { 
  childProcess.spawn(electron, ['--debug=5858','./app'], { stdio: 'inherit' }); 
}); 

In the debug panel choose “Attach” configuration and click run or press F5. After few seconds you should see the debug command panel in the top.

debug-star

Creating the AngularJS Application

New to AngularJS? Check out the official website here or some of the Scotch Angular tutorials.

This section explains how to create a simple Customer Manager application using AngularJS with a MySQL database as the backend. The goal of this application is not to highlight the core concepts of AngularJS but to demonstrate how to use the AngularJS and NodeJS together with MySQL backend inside GitHub Electron.

Our Customer Manager application is as simple as the following:

  • List the customers
  • Add New Customer
  • Option to delete a customer
  • Search for the customer

Project Structure

Our application is located inside app folder, and the structure of the application is shown below.

angular-project-structure

The home page is the app/index.html file. The app/scripts folder contains all the key scripts and views used in this application. There are several techniques that can be used for organizing application files.

Here I prefer scripts organized by features. Each feature has its own folder with templates and controllers inside the same folder. For more info on folder structure, read: AngularJS Best Practices: Directory Structure

Before get started with the AngularJS application, we’re going to install client side dependencies using bower. Install Bower if you don’t have it already. Change the current working directory to the root of the application in your terminal then install dependencies as follows from your command line prompt :


bower install angular angular-route angular-material --save 

Setting Up the Database

For this demo I’ll be using a database called customer-manager and a table called customers. Here is the dump of database so that you can get up and run quickly.


CREATE TABLE `customer_manager`.`customers` ( 
  `customer_id` INT NOT NULL AUTO_INCREMENT, 
  `name` VARCHAR(45) NOT NULL, 
  `address` VARCHAR(450) NULL, 
  `city` VARCHAR(45) NULL, 
  `country` VARCHAR(45) NULL, 
  `phone` VARCHAR(45) NULL, 
  `remarks` VARCHAR(500) NULL, PRIMARY KEY (`customer_id`) 
); 

Creating an Angular Service to Interact with MySQL

Once you have your database and table ready, let’s create an AngularJS service to access the data directly from this database. The service connects to database using node-mysql npm module – a NodeJs driver for MySql written in JavaScript. Install node-mysql module in your app/ folder where your Angular application resides.

Note , we install node-mysql module inside app folder, not in the application root, as we need to include this module inside the final distribution.

Change the current working directory to app folder in your command prompt install using following command.


npm install --save mysql 

Our angular service – app/scripts/customer/customerService.js, should looks like following:

 
(function () {
    'use strict';
    var mysql = require('mysql');
    
    // Creates MySql database connection
    var connection = mysql.createConnection({
        host: "localhost",
        user: "root",
        password: "password",
        database: "customer_manager"
    });
    
    angular.module('app')
        .service('customerService', ['$q', CustomerService]);
    
    function CustomerService($q) {
        return {
            getCustomers: getCustomers,
            getById: getCustomerById,
            getByName: getCustomerByName,
            create: createCustomer,
            destroy: deleteCustomer,
            update: updateCustomer
        };
        
        function getCustomers() {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers";
            connection.query(query, function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }
        
        function getCustomerById(id) {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers WHERE customer_id = ?";
            connection.query(query, [id], function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }
        
        function getCustomerByName(name) {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers WHERE name LIKE  '" + name + "%'";
            connection.query(query, [name], function (err, rows) {
                if (err) deferred.reject(err);
                
                deferred.resolve(rows);
            });
            return deferred.promise;
        }
        
        function createCustomer(customer) {
            var deferred = $q.defer();
            var query = "INSERT INTO customers SET ?";
            connection.query(query, customer, function (err, res) 
                if (err) deferred.reject(err);
                deferred.resolve(res.insertId);
            });
            return deferred.promise;
        }
        
        function deleteCustomer(id) {
            var deferred = $q.defer();
            var query = "DELETE FROM customers WHERE customer_id = ?";
            connection.query(query, [id], function (err, res) {
                if (err) deferred.reject(err);
                deferred.resolve(res.affectedRows);
            });
            return deferred.promise;
        }
        
        function updateCustomer(customer) {
            var deferred = $q.defer();
            var query = "UPDATE customers SET name = ? WHERE customer_id = ?";
            connection.query(query, [customer.name, customer.customer_id], function (err, res) {
                if (err) deferred.reject(err);
                deferred.resolve(res);
            });
            return deferred.promise;
        }
    }
})();

customerService is a simple custom angular service that provides basic CRUD operations on customers table . It uses node’s mysql module directly inside the service. If you already have a remote data service, you can use it instead.

Controller & Template

OurcustomerControllerinsideapp/scripts/customer/customerController is as follows:

 
(function () {
    'use strict';
    angular.module('app')
        .controller('customerController', ['customerService', '$q', '$mdDialog', CustomerController]);
    
    function CustomerController(customerService, $q, $mdDialog) {
        var self = this;
        
        self.selected = null;
        self.customers = [];
        self.selectedIndex = 0;
        self.filterText = null;
        self.selectCustomer = selectCustomer;
        self.deleteCustomer = deleteCustomer;
        self.saveCustomer = saveCustomer;
        self.createCustomer = createCustomer;
        self.filter = filterCustomer;
        
        // Load initial data
        getAllCustomers();
        
        //----------------------
        // Internal functions 
        //----------------------
        
        function selectCustomer(customer, index) {
            self.selected = angular.isNumber(customer) ? self.customers[customer] : customer;
            self.selectedIndex = angular.isNumber(customer) ? customer: index;
        }
        
        function deleteCustomer($event) {
            var confirm = $mdDialog.confirm()
                                   .title('Are you sure?')
                                   .content('Are you sure want to delete this customer?')
                                   .ok('Yes')
                                   .cancel('No')
                                   .targetEvent($event);
            
            
            $mdDialog.show(confirm).then(function () {
                customerService.destroy(self.selected.customer_id).then(function (affectedRows) {
                    self.customers.splice(self.selectedIndex, 1);
                });
            }, function () { });
        }
        
        function saveCustomer($event) {
            if (self.selected != null && self.selected.customer_id != null) {
                customerService.update(self.selected).then(function (affectedRows) {
                    $mdDialog.show(
                        $mdDialog
                            .alert()
                            .clickOutsideToClose(true)
                            .title('Success')
                            .content('Data Updated Successfully!')
                            .ok('Ok')
                            .targetEvent($event)
                    );
                });
            }
            else {
                //self.selected.customer_id = new Date().getSeconds();
                customerService.create(self.selected).then(function (affectedRows) {
                    $mdDialog.show(
                        $mdDialog
                            .alert()
                            .clickOutsideToClose(true)
                            .title('Success')
                            .content('Data Added Successfully!')
                            .ok('Ok')
                            .targetEvent($event)
                    );
                });
            }
        }
        
        function createCustomer() {
            self.selected = {};
            self.selectedIndex = null;
        }
        
        function getAllCustomers() {
            customerService.getCustomers().then(function (customers) {
                self.customers = [].concat(customers);
                self.selected = customers[0];
            });
        }
        
        function filterCustomer() {
            if (self.filterText == null || self.filterText == "") {
                getAllCustomers();
            }
            else {
                customerService.getByName(self.filterText).then(function (customers) {
                    self.customers = [].concat(customers);
                    self.selected = customers[0];
                });
            }
        }
    }

})();

Our Customer template(app/scripts/customer/customer.html) uses angular material components to build user interface, and its is as follows:


<div style="width:100%" layout="row">
    <md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
                md-component-id="left"
                md-is-locked-open="$mdMedia('gt-sm')">

        <md-toolbar layout="row" class="md-whiteframe-z1">
            <h1>Customers</h1>
        </md-toolbar>
        <md-input-container style="margin-bottom:0">
            <label>Customer Name</label>
            <input required name="customerName" ng-model="_ctrl.filterText" ng-change="_ctrl.filter()">
        </md-input-container>
        <md-list>
            <md-list-item ng-repeat="it in _ctrl.customers">
                <md-button ng-click="_ctrl.selectCustomer(it, $index)" ng-class="{'selected' : it === _ctrl.selected }">
                    {{it.name}}
                </md-button>
            </md-list-item>
        </md-list>
    </md-sidenav>

    <div flex layout="column" tabIndex="-1" role="main" class="md-whiteframe-z2">

        <md-toolbar layout="row" class="md-whiteframe-z1">
            <md-button class="menu" hide-gt-sm ng-click="ul.toggleList()" aria-label="Show User List">
                <md-icon md-svg-icon="menu"></md-icon>
            </md-button>
            <h1>{{ _ctrl.selected.name }}</h1>
        </md-toolbar>

        <md-content flex id="content">
            <div layout="column" style="width:50%">
                <br />
                <md-content layout-padding class="autoScroll">
                    <md-input-container>
                        <label>Name</label>
                        <input ng-model="_ctrl.selected.name" type="text">
                    </md-input-container>
                    <md-input-container md-no-float>
                        <label>Email</label>
                        <input ng-model="_ctrl.selected.email" type="text">
                    </md-input-container>
                    <md-input-container>
                        <label>Address</label>
                        <input ng-model="_ctrl.selected.address"  ng-required="true">
                    </md-input-container>
                    <md-input-container md-no-float>
                        <label>City</label>
                        <input ng-model="_ctrl.selected.city" type="text" >
                    </md-input-container>
                    <md-input-container md-no-float>
                        <label>Phone</label>
                        <input ng-model="_ctrl.selected.phone" type="text">
                    </md-input-container>
                </md-content>
                <section layout="row" layout-sm="column" layout-align="center center" layout-wrap>
                    <md-button class="md-raised md-info" ng-click="_ctrl.createCustomer()">Add</md-button>
                    <md-button class="md-raised md-primary" ng-click="_ctrl.saveCustomer()">Save</md-button>
                    <md-button class="md-raised md-danger" ng-click="_ctrl.cancelEdit()">Cancel</md-button>
                    <md-button class="md-raised md-warn" ng-click="_ctrl.deleteCustomer()">Delete</md-button>
                </section>
            </div>
        </md-content>

    </div>
</div>

app.js contains module initialization script and application route config as follows:



(function () {
    'use strict';
    
    var _templateBase = './scripts';
    
    angular.module('app', [
        'ngRoute',
        'ngMaterial',
        'ngAnimate'
    ])
    .config(['$routeProvider', function ($routeProvider) {
            $routeProvider.when('/', {
                templateUrl: _templateBase + '/customer/customer.html' ,
                controller: 'customerController',
                controllerAs: '_ctrl'
            });
            $routeProvider.otherwise({ redirectTo: '/' });
        }
    ]);

})();

Finally here is our index page app/index.html



<html lang="en" ng-app="app">
    <title>Customer Manager</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge"gt;
    <meta name="description" content="">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
    <!-- build:css assets/css/app.css -->
    <link rel="stylesheet" href="../bower_components/angular-material/angular-material.css" />
    <link rel="stylesheet" href="assets/css/style.css" />
    <!-- endbuild -->
<body>
    <ng-view></ng-view>
    <!-- build:js scripts/vendor.js -->
    <script src="../bower_components/angular/angular.js"></script>
    <script src="../bower_components/angular-route/angular-route.js"></script>
    <script src="../bower_components/angular-animate/angular-animate.js"></script>
    <script src="../bower_components/angular-aria/angular-aria.js"></script>
    <script src="../bower_components/angular-material/angular-material.js"></script>
    <!-- endbuild -->
   
    <!-- build:app scripts/app.js -->
    <script src="./scripts/app.js"></script>
    <script src="./scripts/customer/customerService.js"></script>
    <script src="./scripts/customer/customerController.js"></script>
    <!-- endbuild -->
</body>
</html>

Run your application using gulp run command or press Ctrl + Shif + B if you already configured VS Code task runner as above.

angular-app

Build the AngularJS Application

To build our Angular application install gulp-uglify, gulp-minify-css and gulp-usemin packages.


npm install --save gulp-uglify gulp-minify-css gulp-usemin

Open your gulpfile.js and import required modules


  var childProcess = require('child_process'); 
  var electron     = require('electron-prebuilt'); 
  var gulp         = require('gulp'); 
  var jetpack      = require('fs-jetpack'); 
  var usemin       = require('gulp-usemin'); 
  var uglify       = require('gulp-uglify');
  
  var projectDir = jetpack; 
  var srcDir     = projectDir.cwd('./app'); 
  var destDir    = projectDir.cwd('./build');

Clean build directory if it already exists.

 
gulp.task('clean', function (callback) { 
    return destDir.dirAsync('.', { empty: true }); 
});

Copy files into build directory, We don’t need to copy the angular application code using copy function. usemin will do this for us in next section:


gulp.task('copy', ['clean'], function () { 
    return projectDir.copyAsync('app', destDir.path(), { 
        overwrite: true, matching: [ 
            './node_modules/**/*', 
            '*.html', 
            '*.css', 
            'main.js', 
            'package.json' 
       ] 
    }); 
});

Our build task takes our app/index.html with gulp.src() and then we pipe it to usemin. It then writes the output into build directory and replace references in index.html with optimized version of code.

Note: Don’t forget to define usemin blocks inside app/index.html as follows:


<!-- build:js scripts/vendor.js -->
<script src="../bower_components/angular/angular.js"></script>
<script src="../bower_components/angular-route/angular-route.js"></script>
<script src="../bower_components/angular-animate/angular-animate.js"></script>
<script src="../bower_components/angular-aria/angular-aria.js"></script>
<script src="../bower_components/angular-material/angular-material.js"></script>
<!-- endbuild -->
    
<!-- build:app scripts/app.js -->
<script src="./scripts/app.js"></script>
<script src="./scripts/customer/customerService.js"></script>
<script src="./scripts/customer/customerController.js"></script>
<!-- endbuild -->

The build task should look as follows:

 
gulp.task('build', ['copy'], function () { 
  return gulp.src('./app/index.html') 
    .pipe(usemin({ 
      js: [uglify()] 
    })) 
    .pipe(gulp.dest('build/')); 
    }); 

Preparing for Distribution

In this section we will package our Electron application for production. Create your build script in root directory as build.windows.js. This script is intended to be used on Windows. For other platform you should create scripts specific to that platform and should run according to your platform.

A typical electron distribution can be found inside node_modules/electron-prebuilt/dist directory. Here is our steps to build the electron application:

  • Our very first task to do is to copy electron distribution into our dist folder.

  • Each electron distribution contains a default application inside dist/resources/default_app folder. We need to replace this application with our final angular application build.

  • To protect our application’s source code and resources from users, you can choose to package your app into an asar archive with little changes to your source code. An asar archive is a simple tar-like format that concatenate files into a single file, Electron can read arbitrary files from it without unpacking the whole file.

Note: this section describes about packaging in windows platform. All these steps are same for other platforms but paths and files used here is different for other platforms. You can get the full build scripts for OSx and linux here in github.

Install dependencies required to build the electron as following: npm install --save q asar fs-jetpack recedit.

Next initialize our build script as follows:

 
var Q = require('q'); 
var childProcess = require('child_process'); 
var asar = require('asar'); 
var jetpack = require('fs-jetpack');
var projectDir;
var buildDir; 
var manifest; 
var appDir;

function init() { 
    // Project directory is the root of the application
    projectDir = jetpack; 
    // Build directory is our destination where the final build will be placed 
    buildDir = projectDir.dir('./dist', { empty: true }); 
    // angular application directory 
    appDir = projectDir.dir('./build'); 
    // angular application's package.json file 
    manifest = appDir.read('./package.json', 'json'); 
    return Q(); 
} 

Here we use fs-jetpack node module for the file operation. It gives more flexibility on file operations.

Copy Electron Distribution

Copy the default electron distribution from electron-prebuilt/dist to our dist directory.

 
 function copyElectron() { 
     return projectDir.copyAsync('./node_modules/electron-prebuilt/dist', buildDir.path(), { overwrite: true }); 
} 

Cleanup the Default Application

You can find a default HTML application inside resources/default_app folder. We need to replace this application with our angular application. Remove it as follows:

Note: Here path is specific to windows platform. For other platforms process is same but path is different. In OSX it’s Contents/Resources/default_app

 
 function cleanupRuntime() { 
     return buildDir.removeAsync('resources/default_app'); 
} 

Create asar package

 
function createAsar() { 
     var deferred = Q.defer(); 
     asar.createPackage(appDir.path(), buildDir.path('resources/app.asar'), function () { 
         deferred.resolve(); 
     }); 
     return deferred.promise; 
} 

This combines all your angular application files into single asar package file. You can find the destination asar file in dist/resources/ directory.

Replace application resources with your own

Next step is to replace the default electron icon with your own, update the product information and rename the application.

 
function updateResources() {
    var deferred = Q.defer();

    // Copy your icon from resource folder into build folder.
    projectDir.copy('resources/windows/icon.ico', buildDir.path('icon.ico'));

    // Replace Electron icon for your own.
    var rcedit = require('rcedit');
    rcedit(buildDir.path('electron.exe'), {
        'icon': projectDir.path('resources/windows/icon.ico'),
        'version-string': {
            'ProductName': manifest.name,
            'FileDescription': manifest.description,
        }
    }, function (err) {
        if (!err) {
            deferred.resolve();
        }
    });
    return deferred.promise;
}
//Rename the electron exe 
function rename() {
    return buildDir.renameAsync('electron.exe', manifest.name + '.exe');
}

Creating Native Installers

You can either use wix or NSIS to create windows installer. Here we use NSIS which is designed to be small and flexible as possible and is very suitable for internet distribution. With NSIS you can create such installers that are capable of doing everything that is needed to setup your software.

Create NSIS script in resources/windows/installer.nsis

    !include LogicLib.nsh
    !include nsDialogs.nsh

    ; --------------------------------
    ; Variables
    ; --------------------------------

    !define dest "{{dest}}"
    !define src "{{src}}"
    !define name "{{name}}"
    !define productName "{{productName}}"
    !define version "{{version}}"
    !define icon "{{icon}}"
    !define banner "{{banner}}"

    !define exec "{{productName}}.exe"

    !define regkey "Software\${productName}"
    !define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}"

    !define uninstaller "uninstall.exe"

    ; --------------------------------
    ; Installation
    ; --------------------------------

    SetCompressor lzma

    Name "${productName}"
    Icon "${icon}"
    OutFile "${dest}"
    InstallDir "$PROGRAMFILES\${productName}"
    InstallDirRegKey HKLM "${regkey}" ""

    CRCCheck on
    SilentInstall normal

    XPStyle on
    ShowInstDetails nevershow
    AutoCloseWindow false
    WindowIcon off

    Caption "${productName} Setup"
    ; Don't add sub-captions to title bar
    SubCaption 3 " "
    SubCaption 4 " "

    Page custom welcome
    Page instfiles

    Var Image
    Var ImageHandle

    Function .onInit

        ; Extract banner image for welcome page
        InitPluginsDir
        ReserveFile "${banner}"
        File /oname=$PLUGINSDIR\banner.bmp "${banner}"

    FunctionEnd

    ; Custom welcome page
    Function welcome

        nsDialogs::Create 1018

        ${NSD_CreateLabel} 185 1u 210 100% "Welcome to ${productName} version ${version} installer.$\r$\n$\r$\nClick install to begin."

        ${NSD_CreateBitmap} 0 0 170 210 ""
        Pop $Image
        ${NSD_SetImage} $Image $PLUGINSDIR\banner.bmp $ImageHandle

        nsDialogs::Show

        ${NSD_FreeImage} $ImageHandle

    FunctionEnd

    ; Installation declarations
    Section "Install"

        WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR"
        WriteRegStr HKLM "${uninstkey}" "DisplayName" "${productName}"
        WriteRegStr HKLM "${uninstkey}" "DisplayIcon" '"$INSTDIR\icon.ico"'
        WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"'

        ; Remove all application files copied by previous installation
        RMDir /r "$INSTDIR"

        SetOutPath $INSTDIR

        ; Include all files from /build directory
        File /r "${src}\*"

        ; Create start menu shortcut
        CreateShortCut "$SMPROGRAMS\${productName}.lnk" "$INSTDIR\${exec}" "" "$INSTDIR\icon.ico"

        WriteUninstaller "${uninstaller}"

    SectionEnd

    ; --------------------------------
    ; Uninstaller
    ; --------------------------------

    ShowUninstDetails nevershow

    UninstallCaption "Uninstall ${productName}"
    UninstallText "Don't like ${productName} anymore? Hit uninstall button."
    UninstallIcon "${icon}"

    UninstPage custom un.confirm un.confirmOnLeave
    UninstPage instfiles

    Var RemoveAppDataCheckbox
    Var RemoveAppDataCheckbox_State

    ; Custom uninstall confirm page
    Function un.confirm

        nsDialogs::Create 1018

        ${NSD_CreateLabel} 1u 1u 100% 24u "If you really want to remove ${productName} from your computer press uninstall button."

        ${NSD_CreateCheckbox} 1u 35u 100% 10u "Remove also my ${productName} personal data"
        Pop $RemoveAppDataCheckbox

        nsDialogs::Show

    FunctionEnd

    Function un.confirmOnLeave

        ; Save checkbox state on page leave
        ${NSD_GetState} $RemoveAppDataCheckbox $RemoveAppDataCheckbox_State

    FunctionEnd

    ; Uninstall declarations
    Section "Uninstall"

        DeleteRegKey HKLM "${uninstkey}"
        DeleteRegKey HKLM "${regkey}"

        Delete "$SMPROGRAMS\${productName}.lnk"

        ; Remove whole directory from Program Files
        RMDir /r "$INSTDIR"

        ; Remove also appData directory generated by your app if user checked this option
        ${If} $RemoveAppDataCheckbox_State == ${BST_CHECKED}
            RMDir /r "$LOCALAPPDATA\${name}"
        ${EndIf}

    SectionEnd

Create a function called createInstaller in your build.windows.js file as follows:

 

function createInstaller() {
    var deferred = Q.defer();

    function replace(str, patterns) {
        Object.keys(patterns).forEach(function (pattern) {
            console.log(pattern)
              var matcher = new RegExp('{{' + pattern + '}}', 'g');
            str = str.replace(matcher, patterns[pattern]);
        });
        return str;
    }

    var installScript = projectDir.read('resources/windows/installer.nsi');

    installScript = replace(installScript, {
        name: manifest.name,
        productName: manifest.name,
        version: manifest.version,
        src: buildDir.path(),
        dest: projectDir.path(),
        icon: buildDir.path('icon.ico'),
        setupIcon: buildDir.path('icon.ico'),
        banner: projectDir.path('resources/windows/banner.bmp'),
    });
    buildDir.write('installer.nsi', installScript);

    var nsis = childProcess.spawn('makensis', [buildDir.path('installer.nsi')], {
        stdio: 'inherit'
    });

    nsis.on('error', function (err) {
        if (err.message === 'spawn makensis ENOENT') {
            throw "Can't find NSIS. Are you sure you've installed it and"
            + " added to PATH environment variable?";
        } else {
            throw err;
        }
    });

    nsis.on('close', function () {
        deferred.resolve();
    });

    return deferred.promise;

}
 

You should have NSIS installed, and should be available in your path. creaeInstaller function reads installer script and execute it against NSIS runtim with makensis command.

Putting It All Together

Create a function to put all pieces together, then export it to be accessed from gulp task:


   function build() { 
    return init()
            .then(copyElectron) 
            .then(cleanupRuntime) 
            .then(createAsar) 
            .then(updateResources) 
            .then(rename) 
            .then(createInstaller); 
    }
    module.exports = { build: build };

Next create gulp task in gulpfile.js to execute this build script:



 var release_windows = require('./build.windows'); 
 var os = require('os'); 
 gulp.task('build-electron', ['build'], function () { 
     switch (os.platform()) { 
         case 'darwin': 
         // execute build.osx.js 
         break; 
         case 'linux': 
         //execute build.linux.js 
         break; 
         case 'win32': 
         return release_windows.build(); 
     } 
}); 

You can have your final product by running:


gulp build-electron

Your final electron application should be in dist folder and folder structure should be some thing similar to following.

dist-structure

Summary

Electron is not just a native web view that let’s you to wrap your web application into desktop application . It now includes automatic app updates, Windows installers, crash reporting, notifications, and other useful native app features — all exposed through JavaScript APIs.

So far there is huge range of apps created using electron including chat apps, database explorers, map designers, collaborative design tools, and mobile prototyping apps.

Here is some useful resources about Github Electron:

Jasim Muhammed

Full stack web developer with over seven years of experience in building web applications. Passionate about Front-End development . Focused on React, Angular and Sencha ExtJs