Mobile HTML5

Teaching developers how to build great mobile web apps with HTML5.

Curated by Ian Sefferman and supported by AppStoreHQ. With AppStoreHQ, you can list your mobile web app and accept payments easily.

Feb 20

Tutorial: Your First Mobile HTML5 App - Offline Storage / The Local SQL Database (Part 2)

Continuing along with our tutorial series about creating your first mobile HTML5 app, today we’ll talk about the local SQL database available. Part 1 discussed general HTML5 setup, new form elements, and geolocation. We started building a golf score keeper app. Today we’ll continue building that, ripping out some features (to keep our eyes focused on today’s features) and adding some others.

Let’s review with what we ended up with in Part 1, but go ahead and remove the geolocation features:

<!DOCTYPE html> 
<html>  
  <head>
    <title>Golf score keeper</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>
      google.load("jquery", "1.4.1");
    </script>
  </head>
  <body>
    <form method="get" id="score_form">
      <div>
        <label for="1">Hole 1</label>
        <input type="number" min="1" value="4" id="hole1" name="hole1" size="2" step="1" />
      </div>
      <div>
        <label for="2">Hole 2</label>
        <input type="number" min="1" value="4" id="hole1" name="hole2" size="2" step="1" />
      </div>
      <div>
        <input type="email" id="email" placeholder="Enter your email address" size="40"/>
      </div>
      <div>
        <input type="submit" value="Upload Score" />
      </div>
    </form>
  </body>
</html> 

There, just a simple form. Of course, when you have a form, you want to capture the form submission somehow. In our case, we’ll use the new HTML5 local SQL database. We don’t need any servers or HTTP requests or anything except a compatible browser (iPhone’s Safari does the trick). Here’s how we initialize the database:

<!DOCTYPE html> 
<html>  
  <head>
    <title>Golf score keeper</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>
      google.load("jquery", "1.4.1");
    </script>
    <script>
      var db = window.openDatabase("scores", "", "Previous Scores", 1024*1000);
      $(document).ready(function() {
        db.transaction(function(tx) {
          tx.executeSql('CREATE TABLE IF NOT EXISTS Courses(id INTEGER PRIMARY KEY, name TEXT, latitude FLOAT, longitude FLOAT)', []);
          tx.executeSql('CREATE TABLE IF NOT EXISTS Strokes(id INTEGER PRIMARY KEY, course_id INTEGER, hole_num INTEGER, num_strokes INTEGER, email TEXT)', []);
        });
      });
    </script>
  </head>
  <body>
    <form method="get" id="score_form">
      <div>
        <label for="1">Hole 1</label>
        <input type="number" min="1" value="4" id="hole1" name="hole1" size="2" step="1" />
      </div>
      <div>
        <label for="2">Hole 2</label>
        <input type="number" min="1" value="4" id="hole1" name="hole2" size="2" step="1" />
      </div>
      <div>
        <input type="email" id="email" placeholder="Enter your email address" size="40"/>
      </div>
      <div>
        <input type="submit" value="Upload Score" />
      </div>
    </form>
  </body>
</html> 

You can see all we did here was add an openDatabase call and some SQL statements to create tables. These are just standard SQL statements (the reference SQL is from SQLite). Here are the official docs for the API calls.

Now we’ll write a couple functions to help us insert scores into this table:

<!DOCTYPE html> 
<html>  
  <head>
    <title>Golf score keeper</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>
      google.load("jquery", "1.4.1");
    </script>
    <script>
      var db = window.openDatabase("scores", "", "Previous Scores", 1024*1000);

      function insertScore(hole_num, num_strokes, course_id, email) {
       db.transaction(function(tx) {
          tx.executeSql('INSERT INTO Strokes (course_id, hole_num, num_strokes, email) VALUES (?, ?, ?, ?)', [course_id, hole_num, num_strokes, email]);
       });
      }

      $(document).ready(function() {
        db.transaction(function(tx) {
          tx.executeSql('CREATE TABLE IF NOT EXISTS Courses(id INTEGER PRIMARY KEY, name TEXT, latitude FLOAT, longitude FLOAT)', []);
          tx.executeSql('CREATE TABLE IF NOT EXISTS Strokes(id INTEGER PRIMARY KEY, course_id INTEGER, hole_num INTEGER, num_strokes INTEGER, email TEXT)', []);
        });

        $('#score_form').submit(function() {
         strokes = { 1: $('#hole1').val(), 2: $('#hole2').val() };
          for (var hole_num in strokes) {
            insertScore(hole_num, strokes[hole_num], 1, $('#email').val());
          }

          return false;
        });
      });
    </script>
  </head>
  <body>
    <form method="get" id="score_form">
      <div>
        <label for="1">Hole 1</label>
        <input type="number" min="1" value="4" id="hole1" name="hole1" size="2" step="1" />
      </div>
      <div>
        <label for="2">Hole 2</label>
        <input type="number" min="1" value="4" id="hole1" name="hole2" size="2" step="1" />
      </div>
      <div>
        <input type="email" id="email" placeholder="Enter your email address" size="40"/>
      </div>
      <div>
        <input type="submit" value="Upload Score" />
      </div>
    </form>
  </body>
</html> 

Here we added the insertScore function, which performs our SQL INSERT statements, and we add a submit function for the #score_form. When you click the “Upload Score” button, the JavaScript will insert these scores into our new database.

Getting scores inserted into our database was trivial, but now we want to show previously submitted scores, right? This is easy, too. We’ll start with two changes: (1) show ALL previously submitted scores upon loading the page, and (2) update this list whenever new scores are submitted.

<!DOCTYPE html> 
<html>  
  <head>
    <title>Golf score keeper</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>
      google.load("jquery", "1.4.1");
    </script>
    <script>
      var db = window.openDatabase("scores", "", "Previous Scores", 1024*1000);

      function insertScore(hole_num, num_strokes, course_id, email) {
       db.transaction(function(tx) {
          tx.executeSql('INSERT INTO Strokes (course_id, hole_num, num_strokes, email) VALUES (?, ?, ?, ?)', [course_id, hole_num, num_strokes, email]);
       });
      }

      function renderResults(tx, rs) {
        e = $('#previous_scores');
        e.html("");
        for(var i=0; i < rs.rows.length; i++) {
          r = rs.rows.item(i);
          e.html(e.html() + 'id: ' + r['id'] + ', hole_num: ' + r['hole_num'] + ', num_strokes: ' + r['num_strokes'] + ', email: ' + r['email'] + '<br />');
        }
      }

      function renderScores(email) {
        db.transaction(function(tx) {
          if (!(email === undefined)) {
            tx.executeSql('SELECT * FROM Strokes WHERE email = ?', [email], renderResults);
          } else {
            tx.executeSql('SELECT * FROM Strokes', [], renderResults);
          }
        });
      }

      $(document).ready(function() {
        db.transaction(function(tx) {
          tx.executeSql('CREATE TABLE IF NOT EXISTS Courses(id INTEGER PRIMARY KEY, name TEXT, latitude FLOAT, longitude FLOAT)', []);
          tx.executeSql('CREATE TABLE IF NOT EXISTS Strokes(id INTEGER PRIMARY KEY, course_id INTEGER, hole_num INTEGER, num_strokes INTEGER, email TEXT)', []);
        });

        $('#score_form').submit(function() {
         strokes = { 1: $('#hole1').val(), 2: $('#hole2').val() };
          for (var hole_num in strokes) {
            insertScore(hole_num, strokes[hole_num], 1, $('#email').val());
          }

          renderScores();
          return false;
        });

        renderScores();
      });
    </script>
  </head>
  <body>
    <form method="get" id="score_form">
      <div>
        <label for="1">Hole 1</label>
        <input type="number" min="1" value="4" id="hole1" name="hole1" size="2" step="1" />
      </div>
      <div>
        <label for="2">Hole 2</label>
        <input type="number" min="1" value="4" id="hole1" name="hole2" size="2" step="1" />
      </div>
      <div>
        <input type="email" id="email" placeholder="Enter your email address" size="40"/>
      </div>
      <div>
        <input type="submit" value="Upload Score" />
      </div>
    </form>
    <div>
      <h2>Previous Scores</h2>
    </div>
    <div id="previous_scores">

    </div>
  </body>
</html> 

All we did here was add the renderScores (and supporting renderResults) function, call it from our form submission and upon page load, and then add some HTML elements to support the renderScores function. You can see the renderScores function again just uses normal SQL to select rows. The renderResults function updates our HTML elements with the proper rows.

Now our simple app can record scores, and display all those scores back to users without any intervention from outside servers (read: no network access necessary). This is pretty fantastic, but let’s make it easy for a user to keep scores for multiple players at once. To do this, we’ll add another form to filter our scores by email address:

<!DOCTYPE html> 
<html>  
  <head>
    <title>Golf score keeper</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>
      google.load("jquery", "1.4.1");
    </script>
    <script>
      var db = window.openDatabase("scores", "", "Previous Scores", 1024*1000);

      function insertScore(hole_num, num_strokes, course_id, email) {
       db.transaction(function(tx) {
          tx.executeSql('INSERT INTO Strokes (course_id, hole_num, num_strokes, email) VALUES (?, ?, ?, ?)', [course_id, hole_num, num_strokes, email]);
       });
      }

      function renderResults(tx, rs) {
        e = $('#previous_scores');
        e.html("");
        for(var i=0; i < rs.rows.length; i++) {
          r = rs.rows.item(i);
          e.html(e.html() + 'id: ' + r['id'] + ', hole_num: ' + r['hole_num'] + ', num_strokes: ' + r['num_strokes'] + ', email: ' + r['email'] + '<br />');
        }
      }

      function renderScores(email) {
        db.transaction(function(tx) {
          if (!(email === undefined)) {
            tx.executeSql('SELECT * FROM Strokes WHERE email = ?', [email], renderResults);
          } else {
            tx.executeSql('SELECT * FROM Strokes', [], renderResults);
          }
        });
      }

      $(document).ready(function() {
        db.transaction(function(tx) {
          tx.executeSql('CREATE TABLE IF NOT EXISTS Courses(id INTEGER PRIMARY KEY, name TEXT, latitude FLOAT, longitude FLOAT)', []);
          tx.executeSql('CREATE TABLE IF NOT EXISTS Strokes(id INTEGER PRIMARY KEY, course_id INTEGER, hole_num INTEGER, num_strokes INTEGER, email TEXT)', []);
        });

        $('#score_form').submit(function() {
         strokes = { 1: $('#hole1').val(), 2: $('#hole2').val() };
          for (var hole_num in strokes) {
            insertScore(hole_num, strokes[hole_num], 1, $('#email').val());
          }

          renderScores();
          return false;
        });

        $('#filter_previous_scores_form').submit(function() {
          e = $('#filter_by_email').val();
          renderScores(e);
          return false;
        });

        renderScores();
      });
    </script>
  </head>
  <body>
    <form method="get" id="score_form">
      <div>
        <label for="1">Hole 1</label>
        <input type="number" min="1" value="4" id="hole1" name="hole1" size="2" step="1" />
      </div>
      <div>
        <label for="2">Hole 2</label>
        <input type="number" min="1" value="4" id="hole1" name="hole2" size="2" step="1" />
      </div>
      <div>
        <input type="email" id="email" placeholder="Enter your email address" size="40"/>
      </div>
      <div>
        <input type="submit" value="Upload Score" />
      </div>
    </form>
    <div>
      <h2>Previous Scores</h2>
      <form id="filter_previous_scores_form">
        <input type="email" placeholder="Filter scores by email" size="40" id="filter_by_email" /><br />
        <input type="submit" value="Filter" />
      </form>
    </div>
    <div id="previous_scores">

    </div>
  </body>
</html> 

We add the #filter_previous_scores_form form here (complete with placeholder text!) and the associated submit function in our document ready function. When you submit this form, users are able to SELECT just those scores related to the email address entered. Voila!

To me this is an almost fully functioning app (if a bit silly and poorly designed) for allowing one person to keep track of golf scores for multiple players. What’s best about it is that since there are no network calls, the app is (a) fast and (b) usable even without network access.

Still left to do to this app is add even more offline support, talk about background tasks, and learn about how to distribute and promote your mobile HTML5 app. We’ll tackle all these in future parts of this tutorial.


  1. mobilehtml5 posted this