Some initial thoughts on building desktop apps with Ionic and Electron

Earlier this week I was working with a desktop app (which I can't talk about... yet) that had an Ionic-look to it. On a whim, I opened up the package contents and discovered it was an Electron app. If you've never heard of it, Electron is an open source project that lets you build desktop apps (for Mac, Windows, and Linux) using web technologies. The last time I did anything in this space was with Adobe AIR, which was years ago. I've played with Electron a tiny bit, but I had not tried to use Ionic with it so I thought I'd give it a shot. Before digging in, I want to bring up two very important points.

First, I've spent maybe three hours looking into this subject, and obviously have not built a production application yet. This post focuses on some of the things I ran into while testing things out, but it is safe to assume more issues probably exist. As time goes on I'll probably blog other things to consider and I hope my readers will share their own discoveries.

Second, while Electron makes it easy to build a desktop app, I cannot stress enough that you need to remember that app development is not the same as web development. When I talk about Cordova and how it makes it easy to use the web to build mobile apps, I try very hard to remind people that building a mobile app is nothing like building a web page. Repeat that after me: "Building a mobile app is nothing like building a web page."

Ok, so with that out of the way, let's talk shop.

The Basics

The first thing I did was to see what would happen if I took a generic Ionic app and just plain ran it under Electron. To do that, I created a new Ionic app and used the -no-cordova flag. If you aren't aware, you can tell the Ionic CLI to create a new project and skip including all the Cordova bits. You still get a bunch of extra stuff like the bower file and gulp script, so I simply copied out the www folder.

shot1

Then, following the Electron quick start, I added a package.json file and main.js. For my main.js, I copied their code exactly, but removed the bits to open dev tools. (More on that in a bit.)

var app = require('app');  // Module to control application life.
var BrowserWindow = require('browser-window');  // Module to create native browser window.

// Report crashes to our server.
require('crash-reporter').start();

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is GCed.
var mainWindow = null;

// Quit when all windows are closed.
app.on('window-all-closed', function() {
  // On OS X 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();
  }
});

// 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');

  // 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;
  });
});
Here's how my directory structure looked. And again, this is the result of taking the www contents from Ionic's default template (tabs) and adding in the files Electron requires. shot2 At this point I went and just tried running it with the Electron CLI. Surprisingly, it just plain worked. In fact, even the $ionicPlatform.ready fired. As far as I can tell, it noticed that cordova.js didn't load and assumed I was running the app on a desktop browser. So as I said, it just worked, which was cool, but then I began digging in and figuring out bits and pieces of code I needed to rip out and modify. The first thing I did was remove cordova.js since - obviously - this was no longer a Cordova application. Here's the index.html for my app with that modification. I also added a title value. This is going to come into play in a bit.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title>My App</title>

    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">

    <script src="lib/ionic/js/ionic.bundle.js"></script>

    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
    <script src="js/services.js"></script>
  </head>
  
  <body ng-app="starter">
    <ion-nav-bar class="bar-stable">
      <ion-nav-back-button>
      </ion-nav-back-button>
    </ion-nav-bar>

    <ion-nav-view></ion-nav-view>
  </body>
</html>

Next, inside app.js I completely emptied out the run block. We don't need to test for the keyboard plugins, and ionicPlatform.ready does not make sense in this content. Electron lets you do 'Desktop stuff', but doesn't make you wait for an event in your code.

I then noticed something odd. Here's the app running:

shot3

And here it is with another tab selected:

shot4

Did you notice the title? The title of the app changes as I change my view. Now, that could be nice, but to me, it doesn't make sense. The app should have a core title, like "My App", the one I used in html earlier, and the header could continue to be more context-driven. Unfortunately, the code that updates the title is built into Ionic itself and can't be disabled. I raised the issue in an Ionic chat room, and Leandro Zubrezki spent some time helping me out. The following solution is 100% his idea.

In order to prevent the title from updating, you can listen for the $ionicView.afterEnter event and stop it. So for example:

$scope.$on('$ionicView.afterEnter', function(ev, data) { 
      ev.stopPropagation();
});

This can be used in your controllers, but quickly gets repetitive. While talking with Leandro, he suggested using the root level state to define a controller and run it from there. Here is that top level state from the Tabs demo:

.state('tab', {
    url: '/tab',
    abstract: true,
    templateUrl: 'templates/tabs.html',
    controller:'TestCtrl'
})

The change here is the addition of TestCtrl (not the best name). I then added this to the controllers.js file:

.controller('TestCtrl', function($scope) {
  
  console.log('test ctrl');
  $scope.$on('$ionicView.afterEnter', function(ev, data) { 
      ev.stopPropagation();
  });

})

And that was it! It worked fine at that point.

One more tip. Don't forget you can enable dev tools for your app from the menu.

shot5

By default this will appear in the app. Don't forget you can 'pop it' out using the icon highlighted below.

shot7

That icon actually has three states - right, bottom, and popped out. So if it looks like this:

shot8

Clicking will just send it to the bottom. Click and hold to open a menu and select the icon I showed earlier. Using this, I was able to have my dev tools nicely separate from the app which made debugging easier. I also made use of the Reload option too so I didn't have to restart the app from the command line.

shot9

Speaking of the command line, it is worth noting that console.log messages will show up there too.

shot10

Whew - that was a lot. So if you want to see the code behind the modified Tabs demo, you can find it here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/ionic_electron_tabs

A Demo

With a basic understanding of how to make this work in place, I decided to try a desktop version of my RSS Reader. Here it is up and running:

shot11

shot12

Not the most thrilling app, but let me discuss how I changed it for Electron.

I began by making the modifications I mentioned above - removing cordova.js and ionicPlatform.ready. I then needed to do the "title fix" as I mentioned before. This was a bit weird for me as my app did not already have an abstract state on top. I ran into issues with this and got some great help from Mike Hartington. Here is my new app.js with the change to state.

(function() {
/* global angular,window,cordova,console */

    angular.module('starter', ['ionic','rssappControllers','rssappServices'])

    .constant("settings", {
        title:"Raymond Camden's Blog",
        rss:"http://feeds.feedburner.com/raymondcamdensblog"
    })

    .config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {

        $stateProvider
            .state('root', {
                url: '/root',
                abstract: true,
                controller:'RootCtrl',
                template:'<ion-nav-view />'
              })
            .state('root.Home', {
                url: '/home',
                controller: 'HomeCtrl',
                templateUrl: 'partials/home.html'
            })
            .state('root.Entries', {
                url: '/entries',
                controller: 'EntriesCtrl',
                templateUrl: 'partials/entries.html',
            })
            .state('root.Entry', {
                url: '/entry/:index',
                controller: 'EntryCtrl',
                templateUrl: 'partials/entry.html',
            })
            .state('root.Offline', {
                url: '/offline',
                templateUrl: 'partials/offline.html'
            });

        $urlRouterProvider.otherwise("/root/home");

    }])

    .run(['$ionicPlatform','$rootScope','$state', function($ionicPlatform, $rootScope, $state) {

        $rootScope.goHome = function() {
            $state.go("root.Entries");
        };

    }]);

}());

Essentially, my root state has a blank template because that's required in Angular. Well, not blank, but a template with just a nav-view. All I really wanted was my root controller. I won't show the code as it is the same event hack I described above.

Next, I had to make another modification. My RSS Reader app used the Network Information Cordova plugin (and ngCordova) to check to see if you were online. This is somewhat simpler in Electron - you just use navigator.onLine:

if(navigator.onLine) {
    rssService.getEntries(settings.rss).then(function(entries) {
        $ionicLoading.hide();
        $rootScope.entries = entries;
        $state.go("root.Entries");
    });

} else {
    console.log("offline, push to error");
    $ionicLoading.hide();
    $state.go("root.Offline");
}

So far so good. My final change was to the "Read Entry on Site" button. In the Cordova demo, this uses the InAppBrowser. But in Electron, we can actually use a shell command to open a url. The cool thing is that this will then use the user's desired browser to open a new tab:

$scope.readEntry = function(e) {
    var shell = require('shell');
    shell.openExternal(e.link);
};

Pretty cool, right? If you want the full source code, you may find it here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/rssreader_electron

Conclusion

All in all, I think this is an interesting idea. Ionic provides a great UI, and it looks just as good on desktop as it does mobile, and obviously the power of Angular to help architect your application is just as useful here. Certainly there are issues to keep in mind when building a desktop app that won't apply to mobile, but they are things you can address. I'd love to hear what people think, and if you build something (or have built something), please share it in the comments below!

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also