Creating a Simple Single Page App in Electron
Electron is built on Chromium and the navigation model is pretty much the exact same as any other web browser. Not having to go to a remote web server means loading pages in Electron should be pretty quick, but reloading everything switching pages makes it feel like you are using a web app and not a native desktop app. For this reason, It makes sense to build single page apps (SPA) in Electron. In this post I'm going to show you how to do a minimal setup for an SPA in Electron using Sauna and Knockout.
What is Sauna?
Sauna.js is me reinventing the wheel :-), because I want a sauna in my SPA!
Yeah, that was a bad joke, but here is why I am building Sauna... I am learning Electron to build an application, and some of technical decisions I have made for this app are to stay away from things like jQuery and to use Electron's desktop capabilities to the fullest. A lot of the existing SPA frameworks depend on jQuery, and they are built with the limitations of the browser baked into their design. Electron has additional capabilities that normal browser based SPA's do not have, e.g. file system access. Sauna.js will be built to take advantage of those capabilities.
With that said, Sauna.js is available on GitHub here: https://github.com/srakowski/sauna. It is in the early early early phases of development at the time this post is written, so don't critique it too hard. I would appreciate any advice or pull requests to make it better! Currently, the only thing implemented is a simple Navigator object that will load views and bind view models to them. The library requires on Knockout.js. I chose to use Knockout because the framework provides data binding and MVVM without a bunch of additional functionality that I don't need, and it does not depend on jQuery.
I know we need another JS framework like we need another plague, but I think there are some legitimate reasons to build a framework specific to the abilities of Electron. Whether you agree or not, I think it will be fun so I am doing it! Now, let's build the example...
Setting Things Up
I am going to build this project using Visual Studio Code. Check out my previous post Getting Started with Electron in Visual Studio Code to see the initial setup I am working from. I've renamed a few files and added some additional folders to get started. I renamed index.html to app.html (if you do this make sure you update main.js to point to it), and added libs, viewModels, and views folders. The libs folder will hold any third party or custom JavaScript libraries you are building. The viewModels folder will hold the JavaScript files for our ViewModel objects. The views folder will contain the HTML files for our views.
Suana depends on Knockout for data binding. Download Knockout from the website (http://knockoutjs.com/) and add it to your libs folder. Next, download Sauna.js from GitHub here: https://github.com/srakowski/sauna, and add it to your libs folder.
Add an app.js file to your app folder and then copy the following into your app.html file:
<!DOCTYPE html> <html> <head> <title>SPA</title> </head> <body> <div id="app"></div> <script src="libs/knockout-3.3.0.js"></script> <script src="app.js"></script> </body> </html>
The app div will host our views, everything else will remain static. This will allow you to add information or universal commands around the content without reloading it with every view change. We should now have everything in place to get started. Your folder structure should look something like this:
/my_spa /app /libs knockout-3.3.0.js sauna.js /viewModels /views app.html app.js main.js package.json /node_modules ...
Creating Views
The first thing we need to do is create some views. I am going to create a Home view and a Other view. From the Home view you can navigate to the Other view and vice versa. This will demonstrate the basic navigation model available in Sauna.
Add homeView.html to your /views folder. Then paste in the following code:
<div id="views/homeView" data-vm="viewModels/homeViewModel"> <h1>Home</h1> <p> <button data-bind="click: toOtherView">OtherView</button> </p> <p> Link to <a href="#views/otherView">Other View</a> </p> </div>
When creating a view you will need to have a root div with an id that is the same as the path to the view. In the example above homeView.html lives in the /views folder so the id of the div must be "views/homeView" (the .html extension is implied). I may remove this requirement later but maintain backwards compatibility with this tutorial.
Also, you will need to add a data-vm attribute on the root div with the path to the view model you want to bind the view to. In the example above, homeView will be bound to homeViewModel that resides in the viewModels folder. So the data-vm attribute in the root div must be set to "viewModels/homeViewModel" (the .js extension is implied).
The button will use Knockout data bindings and the Sauna navigator to load the OtherView. The link will do the navigation by changing the hash of the location, which is picked up by Sauna and will cause the same navigation to occur.
Now, let's add the Other view and point it back to the home view. In your /views folder add an otherView.html file and copy the following into it:
<div id="views/otherView" data-vm="viewModels/otherViewModel"> <h1>Other</h1> <p> <button data-bind="click: toHomeView">HomeView</button> </p> <p> Link to <a href="#views/homeView">Home View</a> </p> </div>
Now it's time to create our view models...
Creating View Models
Sauna expects the view model to be setup as a Node module exporting the constructor. When a view is loaded Sauna looks up the data-vm tag. It adjusts the path a bit and uses require() to load it. It the creates a new instance of the view model and binds it to the root of the view.
For the example, add homeViewModel.js and otherViewModel.js to the /viewModels folder. Copy the following into homeViewModel.js:
module.exports = function () { var self = this; self.toOtherView = function () { spa.n.navigate("views/otherView"); } };
Next, Copy the following into otherViewModel.js:
module.exports = function () { var self = this; self.toHomeView = function () { spa.n.navigate("views/homeView"); } };
The spa namespace is setup in the section below. The 'n' field refers to the Sauna navigator. For readability I probably should have written out "navigator", but I wanted to type less!
Setting Up Sauna
Setting up Sauna should be pretty easy. Use require to load the module. Sauna exports a constructor. You must pass to it the element you want to host your views in. It gives you back an object with access to the Navigator via the 'n' field.
In your app.js file add the following code:
var spa = null; (function () { document.onreadystatechange = function () { if (document.readyState == "complete") { var sauna = require("./libs/sauna"); spa = sauna(document.getElementById("app")); spa.n.navigate("views/homeView"); } }; })();
Running the SPA
Launch the app in Electron and you should get the following:
There you go! The next step with Sauna.js is to add arguments to navigation. Currently views are loaded, but there is no way to know what to load into them. The code for this tutorial is available on GitHub at: https://github.com/srakowski/sauna-navigation-example