Atom Electron and Webix UI: An Example of Building Cross-platform App

Electron is an open-source framework that allows you to develop cross-platform desktop apps using web technologies such as JavaScript, HTML and CSS.

If you’re a web developer that has an idea of an app that no one can live without, you may wish to enlarge your audience by creating a desktop version as well. But multi-platform desktop application development involves the use of many different technologies, which significantly complicates the task.

image00

The main aim of this article is to learn how you can create cross-platform applications using Webix JavaScript Library and Electron. Besides that, we’ll use Node.js for the server-side part of our project and gulp to build project.

With Electron, you can use your code to build a cross-platform application that is compatible with Mac, Windows and Linux.

To learn how Electron works, we’ll create a To-Do List Application. The app will allow you to add, edit and remove events. Each event has a title, a short description, a location, a date, and a priority. It also has the possibility to filter the existing events.

Creating an Application

Within the project’s folder create the following folders:

  • js that will contain the required JavaScript files
  • css. Here we place the file that defines additional CSS classes
  • the “data” folder will contain some data to be loaded in our application

The project has the following structure:

image01

Within the root folder create a new HTML file named “index.html”.

<!DOCTYPE html>
<html>
    <head>
      <link rel="stylesheet" href="skins/flat.css" type="text/css">
      <link rel="stylesheet" href="css/main.css" type="text/css">
      <script src="codebase/webix.js" type="text/javascript"></script>
    </head>
    <body>
      <script src="bundle/bundle.js" type="text/javascript"></script>
    </body>
</html>

This code will add the JavaScript and CSS files that allow you to use Webix. For our example, we’ll use the Flat skin. The “bundle.js” file that we’ve included in the code above is required to build the project with gulp and Node.js. The “css/main.css” file contains the CSS code necessary for the buttons to look good with the applied skin:

.buttonContainerClass {
  background-color: #3498db;
}
.resetFiltersContainerClass {
  background-color: #3498db;
}
.headerContainerClass {
  border: none;
}
.headerIconContainerClass {
  background-color: #3498db;
  border: none;
}
.changeLocaleClass {
  background-color: #3498db;
  border: none;
}
.dividerClass {
  background-color: #3498db;
  border: none;
}

The “js” folder will contain the logic of our future Electron application. Within this folder create a new file named “logic.js”. This file will contain a bunch of functions required for application to work. They will add rows, remove them and do other useful work. To understand the purpose of each function, check the comments.

Here’s the required code:

/**
 * Get data from backend and fill datatable
 */

function getData() {
    $$("dataFromBackend").clearAll();
    $$("dataFromBackend").load("http://localhost:3000/data");
}
/**
 * Add new row to datatable
 */

function addRow() {
    $$("dataFromBackend").add(
        {
            title: "-----",
            content: "-----",
            place: "-----"
        }
    );
}
/**
 * Reset selection in datatable  */

function clearSelection() {
    $$("dataFromBackend").unselectAll();
}
/**
 * Delete selected row
 */

function deleteRow() {
    if (!$$("dataFromBackend").getSelectedId()) {
        webix.alert(localizator.noItemSelected);
        return;
    }
    //removes the selected item
    $$("dataFromBackend").remove($$("dataFromBackend").getSelectedId());
}
/**
 * Save data to backend from datatable
 */

function saveData() {
    var grid = $$("dataFromBackend");
    var serializedData = grid.serialize();

    // The default Webix header definition is "Content-type": "application/x-www-form-urlencoded"
    // JSON data will not work in this case. Set "Content-Type": "application/json"
    webix.ajax().headers({
        "Content-Type": "application/json"
    }).post("http://localhost:3000/data", {data: serializedData});

    webix.alert(localizator.dataSaved);
}
/**
 * Reset filters settings
 */

function resetFilters() {
    $$("dataFromBackend").getFilter("title").value = null;
    $$("dataFromBackend").getFilter("content").value = null;
    $$("dataFromBackend").getFilter("place").value = null;
    $$("dataFromBackend").getFilter("date").value = null;
    $$("dataFromBackend").getFilter("priority").value = null;

    // reload grid
    $$("dataFromBackend").clearAll();
    $$("dataFromBackend").load("http://localhost:3000/data");
}
/**
 * Function for reserved button
 */

function reservedButton() {
    // your code...
}

This code mostly contains the onclick event handlers. The functions use the methods that allow working with the Webix components.

Now create the “objects.js” file within the same folder. In this file, we’ll define the constructor that works as a wrapper for the standard Webix button control. It is most often used in the application. The purpose of this constructor will be explained later.

Here’s the content of the “js/objects.js” file:

/**
 * Create an object with the type "Button"
 *
 * @constructor
 */

function Button(id, value, type, width, onClickFunction) {
    this.view = "button";
    this.id = id;
    this.value = value;
    this.type = type;
    this.width = width;
    this.on = {
        "onItemClick": function(){
          onClickFunction();
        }
    }
}

Now we have to define how our Electron app will look like. Create a new JavaScript file within the “js” folder. You should name it “structure.js”.

Here’s the required code:

/**
 * Create main layout
 */

webix.ui({
  view: "layout",
  id: "page",
  rows:[
    {
      cols: [
        {
          view:"icon",
          id: "headerIconContainer",
          icon:"calendar"
        },
        {
          view:"template",
          id: "headerContainer",
          type:"header",
          template:"Data master"
          },
          new Button("resetFiltersContainer", "Reset filters", "form", 150, resetFilters),
      ]
    },
    {
      view: "datatable",
      id: "dataFromBackend",
    columns: [
      {
        id: "title",
        header: [
          { text: "<b>Title</b>" },
          { content: "textFilter"}
        ],
        editor: "text",
        fillspace: 2
      },
      {
        id: "content",
        header: [
          { text: "<b>Content</b>" },
          { content: "textFilter" }
        ],
        editor: "popup",
        fillspace: 8
      },
      {
        id: "place",
        header: [
          { text: "<b>Place</b>" },
          { content: "textFilter" }
        ],
        editor: "text",
        fillspace: 2
      },
      {
        id: "date",
        header: [
          "<b>Date</b>",
          { content: "dateFilter" }
        ],
        editor: "date",
        map: "(date)#date#",
        format: webix.Date.dateToStr("%d.%m.%Y"),
        fillspace: 2
      },
      {
        id: "priority",
        header: [
          "<b>Priority</b>",
          { content: "selectFilter" }
        ],
        editor: "select",
        options: [1, 2, 3, 4, 5],
        fillspace: 1
      }
    ],
    editable: true,
    select: "row",
    multiselect: true,
      // initial data load
      data: webix.ajax().get("http://localhost:3000/data")
    },
      {
        view: "layout",
        id: "buttonContainer",
        height: 50,
        cols: [
          // Webix ui.button structure example:
          new Button("loadData", "Load data", "form", 150, getData),
          new Button("addRow", "Add row", "form", 150, addRow),
          new Button("clearSelection", "Clear selection", "form", 150, clearSelection),
          new Button("deleteRow", "Delete row", "form", 150, deleteRow),
          new Button("saveData", "Save data", "form", 150, saveData),
          new Button("reservedButton", "Reserved button", "form", 150, reservedButton),
        {}
      ]
      }
  ]
});

$$("buttonContainer").define("css", "buttonContainerClass");
$$("resetFiltersContainer").define("css", "resetFiltersContainerClass");
$$("headerIconContainer").define("css", "headerIconContainerClass");
$$("headerContainer").define("css", "headerContainerClass");
$$("changeLocale").define("css", "changeLocaleClass");
$$("divider").define("css", "dividerClass");

Let us explain how everything works. The very first view: “layout” property creates a layout that consists of a series of rows and columns. There are three big rows:

  1. The header
  2. The DataTable component that displays our To-Do List
  3. The bottom panel with the buttons such as Load Data, Add Row, Delete Row, etc.

The “id” property allows us to get access to the component using the $$() method. For example, $$(“loadData”) will return the button we’ve created in the code above. The “value” property defines the button’s label, “type” defines its type, and “width” allows us to set its width. Using the “on” object we can create event handlers. In our example, onItemClick corresponds to the onclick event and will call the getData() function.

Instead of using the standard mechanism of creating a button, we have used the constructor previously defined in the “objects.js” file. It returns the Button object in accordance with the passed parameters. Thus, you can avoid code duplication and create an object as follows:

new Button("loadData", "Load data", "form", 150, getData)

There’s also one reserved button that was added for better UX. It does nothing, so you can use it as you like.

There’s also some code at the bottom of the file that looks like this:

$$("buttonContainer").define("css", "buttonContainerClass")

This code allows adding the buttonContainerClass CSS class to the component.

The client-side code of our Electron project is completed, and we can proceed with the backend part of our project.

Server-side Code

Before creating the server, let’s define some data to be loaded automatically after the application starts. It’ll help us to be sure that our Electron app works. Within the “data” folder create a new file named “data.json” with the following testing data:

{
    "data": [
        {
            "title": "Task 1",
            "content": "Do something",
            "place": "Work",
            "date": "2017-02-01 00:00",
            "id": 1485941605794,
            "priority": "4"
        },
        {
            "title": "Task 2",
            "content": "Do nothing",
            "place": "Home",
            "date": "2017-02-04 00:00",
            "id": 1485941605822,
            "priority": "2"
        },
        {
            "title": "-----",
            "content": "-----",
            "place": "-----",
            "date": "",
            "id": 1486460933215
        },
        {
            /* some more data */
        }
    ]
}

Our server side will be based on Node.js and Express. Create a new file named “server.js” within the root folder of your application.

Here’s the required server code:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
var cors = require('cors');
var path = require("path");
const app = express();
const port = 3000;

// use to parse json data
app.use(bodyParser.json());

// use to create cross-domain requests (CORS)
app.use(cors());

// create path aliases to use them in the “index.html” file
// otherwise the assets in it will not work and icons will not be shown
// scheme:
// app.use('/my_path_alias', express.static(path.join(__dirname, '/path_to_where/my_assets_are')));
app.use('/css', express.static(path.join(__dirname, '/css')));
app.use('/skins', express.static(path.join(__dirname, '/codebase/skins')));
app.use('/bundle', express.static(path.join(__dirname, '/')));
app.use('/codebase', express.static(path.join(__dirname, '/codebase')));
app.use('/fonts', express.static(path.join(__dirname, '/codebase/fonts')));

const filePath = __dirname + '/data/';
const fileName = "data.json";

/**
 * Get index page
 *
 * @param {string} URL
 * @param {function} Callback
 */

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname + '/index.html'));
});
/**
 * Send GET request to get data
 *
 * @param {string} URL
 * @param {function} Callback
 */

app.get('/data', (req, res) => {
  const options = {
    root: filePath
  };

  res.sendFile(fileName, options, function (err) {
    if (err) {
      console.log('Error:', err);
    } else {
      console.log('Received:', fileName);
    }
  });
});
/**
 * Send POST request to save data
 *
 * @param {string} URL
 * @param {function} Callback
 */

app.post('/data', (req, res) => {
  // use JSON.stringify() 2nd and 3rd param to create pretty JSON data
  // remove them for minified JSON
  fs.writeFile(filePath + fileName, JSON.stringify(req.body, null, 4), 'utf-8', (err) => {
    if (err) {
      console.log('Error:', err);
    }
    res.status(200).send(req.body);
  });
});
/**
 * Listen to server with specified port
 *
 * @param {string} Port
 * @param {function} Callback
 */

app.listen(port, () => {
  // open browser on http://localhost:3000
  console.log('Server is running on http://localhost:' + port);
});

This code contains some comments that describe how everything works.

Now we can create the files required for building our cross-platform application.

Building the Project

Before building our Electron project, we have to create a file that will contain its main settings. Within the root folder of your project create a new file named “package.json” and paste the following code in it:

{
    "name": "data_master",
    "description": "Simple ToDo list with desktop building",
    "version": "0.1.0",
    "tags": [
        "node.js",
        "webix",
        "electron",
        "express",
        "ToDo list"
    ],
    "main": "main.js",
    "scripts": {
        "start": "electron .",
        "package": "electron-packager ./ DataMaster --all --out ~/release/DataMaster --overwrite",
        "nodemon": "nodemon server.js",
        "server": "node server.js"
    },
    "dependencies": {
        "electron-prebuilt": "^0.35.6",
        "electron-packager": "^8.4.0",
        "express": "^4.14.0",
        "body-parser": "^1.16.0",
        "cors": "^2.8.1"
    },
    "devDependencies": {
        "gulp": "^3.9.0",
        "gulp-concat": "^2.6.0",
        "gulp-uglify": "^1.2.0",
        "gulp-sourcemaps": "^1.5.2",
        "nodemon": "^1.11.0"
    },
    "license": "GPL-3.0"
}

This file contains the Node.js dependencies. To install them run the following command:

npm install

This command will create a new folder named “node_modules” and load the required files into it.
The file “gulpfile.js” within the root folder of the project defines the config of our package:

var gulp = require('gulp'),
    uglify = require('gulp-uglify'),
    concat = require('gulp-concat');
    // to create source mapping
    sourcemaps = require('gulp-sourcemaps');
/*
 * Collect all js files to one bundle script
 * Command: "gulp bundle"
 */

gulp.task('bundle', function() {
    // choose any files in directories and their subfolders
    return gulp.src('js/**/*.js')
        .pipe(sourcemaps.init())
        .pipe(concat('bundle.js'))
        .pipe(sourcemaps.write('./'))
        //.pipe(uglify())
        // output result to current directory
        .pipe(gulp.dest('./'));
});
/*
 * Watch js files changing and run task
 * Command: "gulp watch"
 */

gulp.task('watch', function () {
    gulp.watch('./js/**/*.js', ['bundle']);
});

Now we can build our Electron project using the following command:

gulp bundle

The gulp watch command allows you to track the state of JavaScript files during the development and run gulp bundle if some changes occurred.
You can test your web app now. To start the server, use the following command:

npm run server

If you open http://localhost:3000/ in your browser, you’ll get the following result:

image02

Everything works well, which means we can create our cross-platform desktop application.

Creating the Cross-platform Desktop App

To create a desktop app, we need to install Electron. The “package.json” file contains the description of two modules that will do the main job. The electron-prebuilt module is required for pre-assembling and running the application. The electron-packager in its turn allows you to compile applications for the particular target platform or for all supported platforms.

To install these modules, you can use the following commands:

npm install --save-dev electron-prebuilt
npm install --save-dev electron-packager

There’s also something important in the “package.json” file:

"scripts": {
   "start": "electron .",
    "package": "electron-packager ./ DataMaster --all --out ~/release/DataMaster --overwrite",
},

This code section allows us to run pre-assembling using npm start and then run the compiling with the npm run-script package command. By changing the package command we can define the target platform. For example this code will create an app for Windows x64:

"package": "electron-packager ./ DataMaster --win32-x64 --out ~/release/DataMaster --overwrite"

Within the root folder of your app create a new file named “main.js”:

/*
 * Commands:
 * npm init - initialize npm in the current directory
 * npm install - install modules
 * npm install --save-dev electron-prebuilt - install module for pre-build
 * npm install --save-dev electron-packager - install module for build
 * npm start - to start app
 * npm run-script package - to compile app
 */

const electron = require('electron');
// lifecycle of our app
const app = electron.app;
// create window for our app
const BrowserWindow = electron.BrowserWindow;

// To send crash reports to Electron support
// electron.crashReporter.start();

// set global link
// if not, the window will be closed after garbage collection
var mainWindow = null;

/**
 * Check that all windows are closed before quitting app
 */

app.on('window-all-closed', function() {
    // OS X apps are active before "Cmd + Q" command. Close app
    if (process.platform != 'darwin') {
        app.quit();
    }
});
/**
 * Create main window menu
 */

function createMenu() {
    var Menu = electron.Menu;
    var menuTemplate = [
        {
            label: 'File',
            submenu: [
                { label: 'New window', click: function() {
                     createSubWindow();
                 }},
                { type: "separator"},
                { label: 'Exit', click: function() {
                     app.quit();
                 }}
            ]
        },
        {
            label: 'Edit',
            submenu: [
                { label: 'Cut', role: 'cut' },
                { label: 'Copy', role: 'copy' },
                { label: 'Paste', role: 'paste'}
            ]
        },
        {
            label: 'About',
            submenu: [
                { label: 'Name', click: function() {
                     console.log(app.getName());
                 }},
                { label: 'Version', click: function() {
                     console.log(app.getVersion());
                 }},
                { label: 'About', click: function() {
                     console.log('ToDo list');
                 }}
            ]
        },
        {
            label: 'Help',
            submenu: [
                { label: 'Node.js docs', click: function() {
                     require('electron').shell.openExternal("https://nodejs.org/api/");
                 }},
                { label: 'Webix docs', click: function() {
                     require('electron').shell.openExternal("http://docs.webix.com/");
                 }},
                { label: 'Electron docs', click: function() {
                     require('electron').shell.openExternal("http://electron.atom.io/docs/all");
                 }}
            ]
        }
    ];

    var menuItems = Menu.buildFromTemplate(menuTemplate);
    Menu.setApplicationMenu(menuItems);
}
/**
 * Create main window
 */

function createMainWindow() {
    mainWindow = new BrowserWindow({
        title: "Data master",
        resizable: false,
        width: 910,
        height: 800,
        // set path to icon for compiled app
        icon: 'resources/app/img/icon.png',
        // set path to icon for launched app
        //icon: 'img/icon.png'
        center: true
        // to open dev console: The first way
        //devTools: true
    });

    createMenu();

    // load entry point for desktop app
    //mainWindow.loadURL('npm//' + __dirname + '/index.html');
    mainWindow.loadURL('http://localhost:3000/');

    // to open dev console: The second way
    //mainWindow.webContents.openDevTools();

    // Close all windows when the main window is closed
    mainWindow.on('closed', function() {
        mainWindow = null;
        newWindow = null;
    });
}
/**
 * Create a sub menu window
 */

function createSubWindow() {
    newWindow = new BrowserWindow({
        title: "Go to GitHub",
        resizable: false,
        // imitate mobile device
        width: 360,
        height: 640,
        icon: 'resources/app/img/mobile.png',
        center: true
    });

    newWindow.loadURL("https://github.com/");

    newWindow.on('closed', function() {
        newWindow = null;
    });
}
/**
 * When Electron finishes initialization and is ready to create browser window
 */

app.on('ready', function() {
    createMainWindow();
});

This file defines the configuration of Electron. The code creates a new electron object. Then it creates and configures the application window. The main application URL is passed to this window. For example:

mainWindow.loadURL('file://' + __dirname + '/index.html')

In our case it’s the “index.html” file from the root folder.

The “mainWindow = null” code line removes the link to the window. It’s required because in case if the Electron application supports multiple windows, we need to catch the moment when it’s time to remove the corresponding element. In our case, this action will close the child window.

Finally, you can run the command that will build applications for different target platforms:

npm run-script package

You can find ready desktop apps within the ~/release/DataMaster folder.

image04

Source code

You can get the source code of the described application on Github, and play around with it following the provided instructions.

Webix Can Help You Create Cross Platform Web Apps

The main feature of Webix is the ability to create advanced user interfaces for web applications with minimum effort. The functionality of web application that we’ve created in this tutorial can be significantly extended.

The native HTML5 solution for JavaScript file upload implemented by Webix can be used to visualize the process of uploading the local files to your To-Do-List. If you’re intended to make both web and desktop versions of the application convenient to everyone, Webix web components will rid you of the need to worry about the accessibility issues.