Creating a mobile app from a simple HTML site: Part 3

Adding a server to separate the app from its data

This is the third part in our series of posts about creating a dynamic mobile app from a simple HTML site. In Part 2 we separated the data from its visual representation, but the data is still contained inside the app. In this article, we take that data, remove it from the app altogether, and serve it from the server instead, to maximise reusability between different users.

We will go through the steps required to create a suitable server using NodeJS. Then we will make the app work as a client of this server.

If you have your code handy from last time, then you are ready to go. Make sure you have NodeJS installed. If you don’t have a previous code base setup you can follow the instructions on how to load any stage in this tutorial. Use stage5 as a starting point.

Refactoring our code

Let’s quickly look at the code created in part 2.

Two things happen in the app.onDeviceReady method (see js/index.js) — data is loaded from a JSON file and touch events are listened for on <brick-deck id="plan-group">. After data is loaded, app.renderData parses the JSON string and app.createUI creates a UI for each plan separately.

onDeviceReady and createUI are currently not very efficient, so let’s update them. Let’s split onDeviceReady by creating a new method called activateFingerSwipe that will contain all the code after the request.send() line and simply call it without arguments. Update your code as follows:

onDeviceReady: function() {
     var request = new XMLHttpRequest();
     request.onload = app.renderData;
     request.open("get", "app_data/plans.json", true);
     request.send();

     app.activateFingerSwipe();
},

activateFingerSwipe: function() {
    // Switching from one tab to another is done automatically
    // We just need to link it backwards - change menu if slides
    // changed without touching the menu
    app.planGroupMenu = document.getElementById('plan-group-menu');
    
    // Implementing one finger swipe to change deck card
    app.planGroup = document.getElementById('plan-group');

    var startX = null;
    var slideThreshold = 100;

    // ...

Now, let’s move on to updating createUI. Instead of having the code to create the UI for each individual plan inside createUI, we will create a new user-defined object type called Plan and instantiate it in the loop. Update app.renderData to look like the block below:

renderData: function() {
    var plans = JSON.parse(this.responseText);
    var deck = document.getElementById('plan-group');
    var tabbar = document.getElementById('plan-group-menu');
    navigator.globalization.getDateNames(function(dayOfWeek){
      for (var i = 0; i < plans.length; i++) {
          var plan = new Plan(plans[i]);
          plan.createUI(deck, tabbar, dayOfWeek);
      }
    }, function() {}, {type: 'narrow', item: 'days'});
},

We will define the Plan type as an object having two methods — createUI and selectTab — which are copied from the existing methods and functions. The only changes are related to the data now having an object nature. Instead of plan.week we use this.schedule. this always relates to the current plan’s scope. All changes related to storing parameters in the object are as follows:

  • plan.title -> this.title
  • plan.week -> this.schedule
  • plan.id -> this.id
  • plan.active -> this.active
  • var tab -> this.tab
  • var card -> this.card
  • var table -> this.table
  • selectTab(deck, tab) -> this.selectTab(deck)

At the top of your index.js file, add the following code:

function Plan(plan) {
    this.schedule = plan.week;
    this.title = plan.title;
    this.id = plan.id;
    this.active = plan.active;
    this.tab = null;
    this.card = null;
    this.table = null;
};

Plan.prototype.selectTab = function(deck) {
    var self = this;
    function selectActiveTab() {
        if (!self.tab.targetElement) {
            return window.setTimeout(selectActiveTab, 100);
        }
        deck.showCard(self.tab.targetElement);
    }
    selectActiveTab();
}

Plan.prototype.createUI = function(deck, tabbar, dayOfWeek) {
    // create card
    this.card = document.createElement('brick-card');
    this.card.setAttribute('id', this.id);
    deck.appendChild(this.card);

    //create tab
    this.tab = document.createElement('brick-tabbar-tab');
    this.tab.setAttribute('target', this.id);
    this.tab.appendChild(document.createTextNode(this.title));
    tabbar.appendChild(this.tab);

    // link card and tab DOM Elements
    this.card.tabElement = this.tab;
    this.card.addEventListener('show', function() {
        this.tabElement.select();
    });

    // create plan table
    this.table = document.createElement('table');

    var numberOfDays = this.schedule.length;
    var cleanPlan = [];
    for (var j = 0; j  0) {
            cleanPlan.push(this.schedule[j]);
        }
    }

    var daysInHours = [];
    for (j = 0; j < cleanPlan.length; j++) {
        for (var k = 0; k < cleanPlan[j].length; k++) {
            if (!daysInHours[k]) {
                daysInHours[k] = [];
            }
            daysInHours[k][j] = cleanPlan[j][k];
        }
    }

    for (var j = 0; j < daysInHours.length; j++) {
        var tr = this.table.insertRow(-1);
        var td = tr.insertCell(-1);
        td.appendChild(document.createTextNode(j + 1));
        for (var k = 0; k < cleanPlan.length; k++) {
            var td = tr.insertCell(-1);
            if (daysInHours[j][k]) {
                td.appendChild(document.createTextNode(daysInHours[j][k]));
            }
        }
    }

    var thead = this.table.createTHead();
    var tr = thead.insertRow();
    var th_empty = document.createElement('th');
    tr.appendChild(th_empty);
    var weekDayNumber;
     for (var j = 0; j  0) {
            var th = document.createElement('th');
            th.appendChild(document.createTextNode(dayOfWeek.value[weekDayNumber]));
            tr.appendChild(th);
        }
    }

    this.card.appendChild(this.table);

    if (this.active) {
      this.selectTab(deck);
    }
}

You can check your work against the full refactored code source in github.

You might notice var self = this; and then deck.showCard(self.tab.targetElement); in Plan.prototype.selectTab. this in scope of selectActiveTab would represent a different value then outside of it.

Read more about the new operator and the this keyword on MDN if you want more information.

Building the server

Building a server using NodeJS is fairly simple — http is the only module required, although we will also include fs (as we need to load the file from disk) and sys (to display logs.)

We will place the server’s code into a separate directory (it’s named stage6-server/ in the related Github project) as it is not part of the app.

First of all, create a new directory in the same level as school-plan called server. Move the plans.json file out from the school-plan/www/app-data directory to the server directory, then remove school-plan/www/app-data. You could do these actions using the following terminal commands from school-plan‘s parent directory:

mkdir server
mv school-plan/www/app_data/plans.json server/
rm -rf school-plan/www/app_data

Now onto the NodeJS server code. Create a new file called server.js inside the server directory, and fill it with the following content — this simply reads the file from disk:

var fs = require('fs'),
    sys = require("sys");

fs.readFile(__dirname + "/plans.json", function (err, data) {
    sys.puts(data);
});

Run it with the terminal command node server.js — you should see the content of the plans.json file listed in terminal.

Now we will serve this file using the http module. Modify your NodeJS code like so:

var fs = require('fs'),
    sys = require("sys"),
    http = require('http');

http.createServer(function (request, response) {
  fs.readFile(__dirname + "/plans.json", function (err, data) {
    response.writeHead(200, {"Content-Type": "application/json"});
    response.end(data);
    sys.puts("accessed");
  });
}).listen(8080);
sys.puts("Server Running on http://127.0.0.1:8080");

First we’ve created the server using the http.createServer method and ordered it to listen on port 8080. On every single request, the plans.json file is read and returned as an HTTP response. Then we simply log accessed to the terminal.

Test your file again, as before.

Navigate to http://127.0.0.1:8080 using your browser and you should see the contents of the file inside the browser. (I use the JSON View add-on to make the code look nice too — it’s worth getting.)

So we’re able to serve the file from our machine — now we just need to read it in our app.

Note: The server is serving for any address – it works on http://localhost:8080 and also on your local network address (usually http://192.168.xx.xx:8080). This address should be used when testing the app on a device.

Reading the data from the server

We need to change the app to read from the server instead of the file distributed inside the app. Let’s change the request.open call (inside deviceReady method) to read from the URL instead of the local file. Replace app_data/plans.json with http://127.0.0.1:8080, so it will look like this:

request.open("get", "http://127.0.0.1:8080", true);

Now for a test. Run the cordova prepare command in the terminal, and reload the app in WebIDE.

You will receive an error in the browser console: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://127.0.0.1:8080/. This can be fixed by moving the resource to the same domain or enabling CORS..

CORS, and cross-origin fixes

The error is due to a CORS security policy preventing us from loading a script from a different domain.

There are two ways of loading a script from a different domain. Whitelisting it in Cordova is one of them, but as we’ve got control over the server, we can simply allow cross-site HTTP requests. This is done by setting a header call Access-Control-Allow-Origin in the request. Its value is the domain name(s) that can request content from the server. As our app will be used on phones, all domains should have access. The needed headers are as follows:

"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept"

We will add these headers to the existing Content-Type one specified in our server.js script — update the response.writeHead() call to the following:

response.writeHead(200, {
  "Content-Type": "application/json",
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept"});

Save your NodeJs code, stop (Control + C) the server running in your terminal, and run it again. The application should now be able to read from the server as desired.

If you are having problems getting the app to work, check your code for the app and server.

Serving and loading user plans

Currently we load all plans available in plans.json. As we want to be able to serve plans for any user we need to identify and serve a set.

We will need to identify a plan. To find a plan in our “database” (JSON file) we will change the plan structure from array to object. For example:

{
  "somehashtag": {
  "title": "A",
  "id": "a",
  "active": 1,
  "week": [
    // ...
  ]
}, {
  "otherhashtag": {
  "title": "B",
  "id": "b",
  "week": [
    // ...
  ]
}

We also need to find a way to store hashtags. Most of the users will have a different list of them. Should they be stored only on the device or on the server? Considering that many users change their phone fairly often, it’s best to keep the list of hashtags on the server. In production we would have separate user accounts, but for the sake of this tutorial and the simplicity of the server’s code no security is provided.

We again need to modify the JSON file, to allow for different users and separate plans:

{
  "users": {
    "userA": {
      "name": "John Doe",
      "plans": ["somehashtag", "otherhashtag"]
    },
    // ...  
  },
  "plans": {
    "somehashtag": {
    "title": "A",
    "id": "a",
    "active": 1,
    "week": [
      // ...
    ]
  }, {
    "otherhashtag": {
    "title": "B",
    "id": "b",
    "week": [
      // ...
    ]
  }
}

From now on, data.plans will represent all plans and data.users will represent all of the users.

At this point, download the updated plans.json file from Github, and replace old with new.

I defined the URL to access a user’s plans as follows:

http://{address}/plans/{userid}

Reading URLs is easy with the url module — now you should add it to the loaded modules:

var fs = require('fs'),
    sys = require("sys"),
    http = require('http'),
    url = require('url');

Now remove the current response.end(); and sys.puts() calls, replacing them with:

var args = url.parse(request.url).pathname.split('/');
var command = args[1];
var data = JSON.parse(data);

if (command === 'plans') {
    // get user from data.users
    var userid = args[2];
    var user = data.users[userid];
    var plans = [];
    var i, hashtag;
    for (i = 0; i < user.plans.length; i++) {
       hashtag = user.plans[i];
       // copy each user's plan to plans
       plans.push(data.plans[hashtag]);
    }
    response.end(JSON.stringify(plans));
    sys.puts("accessed user - " + userid);
}

The only change in the app’s usage now is that we can load the data for different users using the new URL scheme:

http://127.0.0.1:8080/plans/someuser

Go to your index.js file and update the following:

request.open("get", "http://127.0.0.1:8080/", true);

to

request.open("get", "http://127.0.0.1:8080/plans/johndoe", true);

Conclusion

We have now provided a way to display different plans for different users. However, we are still downloading the plans every time the app is used, which is not much good for an offline experience, plus we currently can’t change the plans displayed unless we edit the XHR call in the code itself. We’ll tackle these problems in our next article.

For now, check out the final code for the app and server. And look out for Part 4, coming in May!

About Chris Mills

Chris Mills is a senior tech writer at Mozilla, where he writes docs and demos about open web apps, HTML/CSS/JavaScript, A11y, WebAssembly, and more. He loves tinkering around with web technologies, and gives occasional tech talks at conferences and universities. He used to work for Opera and W3C, and enjoys playing heavy metal drums and drinking good beer. He lives near Manchester, UK, with his good lady and three beautiful children.

More articles by Chris Mills…

About Piotr Zalewa

Piotr Zalewa is a Senior Web Developer at Mozilla's Dev Ecosystem team. Working on web apps. He is the creator of JSFiddle.

More articles by Piotr Zalewa…


4 comments

  1. jens

    Part 2 link is broken

    April 30th, 2015 at 03:00

    1. Chris Mills

      Argh, the href got fragged somehow. thanks for the note – fixed now.

      April 30th, 2015 at 03:19

  2. Daniel

    Where are the Part 2? Is not in the Cordova Tag

    May 5th, 2015 at 14:00

    1. Piotr Zalewa

      Thanks for the heads up – tagged. There was also a link at the beginning of the article.

      May 5th, 2015 at 14:26

Comments are closed for this article.