Sample Mobile App with Backbone.js, PhoneGap, and a Local Database

In my previous post, I shared a simple Wine Cellar application built with Backbone.js and packaged as a mobile app with PhoneGap. That version of the application gets its data from a set of RESTful services, which means that you can only use it while online.

In this post, we explore an offline version of the same application: this new version gets its data from your device’s local database using the PhoneGap SQL database API.

By default, Backbone.js Models access their data using RESTful services defined by the Models “url” and “urlRoot” attributes. But Backbone.js also makes it easy and elegant to change your Models data access mechanism: All you have to do is override the Backbone.sync method. From the Backbone.js documentation:

Backbone.sync is the function that Backbone calls every time it attempts to read or save a model to the server. By default, it uses (jQuery/Zepto).ajax to make a RESTful JSON request. You can override it in order to use a different persistence strategy, such as WebSockets, XML transport, or Local Storage.

The documentation includes a useful example that shows how to replace the default Backbone.sync implementation with localStorage-based persistence where models are saved into JSON objects.

In our application, we will replace the default Backbone.sync implementation with SQL-based persistence using the device’s local database.

First, we create a data access object that encapsulates the logic to read (find), create, update, and delete wines. This simple application is read-only and only needs to retrieve collections. So, I only implemented the findAll() method and stubbed the other methods (find, create, update, destroy). WineDAO also has a “populate” function to populate the wine table with sample data.

window.WineDAO = function (db) {
    this.db = db;
};

_.extend(window.WineDAO.prototype, {

    findAll:function (callback) {
        this.db.transaction(
            function (tx) {
                var sql = "SELECT * FROM wine ORDER BY name";
                tx.executeSql(sql, [], function (tx, results) {
                    var len = results.rows.length;
                    var wines = [];
                    for (var i = 0; i < len; i++) {
                        wines[i] = results.rows.item(i);
                    }
                    callback(wines);
                });
            },
            function (tx, error) {
                alert("Transaction Error: " + error);
            }
        );
    },

    create:function (model, callback) {
//        TODO: Implement
    },

    update:function (model, callback) {
//        TODO: Implement
    },

    destroy:function (model, callback) {
//        TODO: Implement
    },

    find:function (model, callback) {
//        TODO: Implement
    },

//  Populate Wine table with sample data
    populate:function (callback) {
        this.db.transaction(
            function (tx) {
                console.log('Dropping WINE table');
                tx.executeSql('DROP TABLE IF EXISTS wine');
                var sql =
                    "CREATE TABLE IF NOT EXISTS wine ( " +
                        "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                        "name VARCHAR(50), " +
                        "year VARCHAR(50), " +
                        "grapes VARCHAR(50), " +
                        "country VARCHAR(50), " +
                        "region VARCHAR(50), " +
                        "description TEXT, " +
                        "picture VARCHAR(200))";
                console.log('Creating WINE table');
                tx.executeSql(sql);
                console.log('Inserting wines');
                tx.executeSql("INSERT INTO wine VALUES (1,'CHATEAU DE SAINT COSME','2009','Grenache / Syrah','France','Southern Rhone / Gigondas','The aromas of fruit and spice give one a hint of the light drinkability of this lovely wine, which makes an excellent complement to fish dishes.','saint_cosme.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (2,'LAN RIOJA CRIANZA','2006','Tempranillo','Spain','Rioja','A resurgence of interest in boutique vineyards has opened the door for this excellent foray into the dessert wine market. Light and bouncy, with a hint of black truffle, this wine will not fail to tickle the taste buds.','lan_rioja.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (3,'MARGERUM SYBARITE','2010','Sauvignon Blanc','USA','California Central Cosat','The cache of a fine Cabernet in ones wine cellar can now be replaced with a childishly playful wine bubbling over with tempting tastes of black cherry and licorice. This is a taste sure to transport you back in time.','margerum.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (4,'OWEN ROE \"EX UMBRIS\"','2009','Syrah','USA','Washington','A one-two punch of black pepper and jalapeno will send your senses reeling, as the orange essence snaps you back to reality. Do not miss this award-winning taste sensation.','ex_umbris.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (5,'REX HILL','2009','Pinot Noir','USA','Oregon','One cannot doubt that this will be the wine served at the Hollywood award shows, because it has undeniable star power. Be the first to catch the debut that everyone will be talking about tomorrow.','rex_hill.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (6,'VITICCIO CLASSICO RISERVA','2007','Sangiovese Merlot','Italy','Tuscany','Though soft and rounded in texture, the body of this wine is full and rich and oh-so-appealing. This delivery is even more impressive when one takes note of the tender tannins that leave the taste buds wholly satisfied.','viticcio.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (7,'CHATEAU LE DOYENNE','2005','Merlot','France','Bordeaux','Though dense and chewy, this wine does not overpower with its finely balanced depth and structure. It is a truly luxurious experience for the senses.','le_doyenne.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (8,'DOMAINE DU BOUSCAT','2009','Merlot','France','Bordeaux','The light golden color of this wine belies the bright flavor it holds. A true summer wine, it begs for a picnic lunch in a sun-soaked vineyard.','bouscat.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (9,'BLOCK NINE','2009','Pinot Noir','USA','California','With hints of ginger and spice, this wine makes an excellent complement to light appetizer and dessert fare for a holiday gathering.','block_nine.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (10,'DOMAINE SERENE','2007','Pinot Noir','USA','Oregon','Though subtle in its complexities, this wine is sure to please a wide range of enthusiasts. Notes of pomegranate will delight as the nutty finish completes the picture of a fine sipping experience.','domaine_serene.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (11,'BODEGA LURTON','2011','Pinot Gris','Argentina','Mendoza','Solid notes of black currant blended with a light citrus make this wine an easy pour for varied palates.','bodega_lurton.jpg')");
                tx.executeSql("INSERT INTO wine VALUES (12,'LES MORIZOTTES','2009','Chardonnay','France','Burgundy','Breaking the mold of the classics, this offering will surprise and undoubtedly get tongues wagging with the hints of coffee and tobacco in perfect alignment with more traditional notes. Breaking the mold of the classics, this offering will surprise and undoubtedly get tongues wagging with the hints of coffee and tobacco in perfect alignment with more traditional notes. Sure to please the late-night crowd with the slight jolt of adrenaline it brings.','morizottes.jpg')");
            },
            function (tx, error) {
                alert('Transaction error ' + error);
            },
            function (tx) {
                callback();
            }
        );
    }
});

I then added a “dao” attribute to the Models to specify which data object should be used to access their underlying data.

window.Wine = Backbone.Model.extend({
	urlRoot: "http://coenraets.org/backbone-cellar/part1/api/wines",
    dao: WineDAO
});

window.WineCollection = Backbone.Collection.extend({
	model: Wine,
	url: "http://coenraets.org/backbone-cellar/part1/api/wines",
    dao: WineDAO
});

NOTE: The application doesn’t need the url and urlRoot attributes anymore, but you can imagine a more sophisticated version of the app that would work with the RESTful services while online and fall back to the local database while offline.

With that infrastructure in place, we can now override Backbone.sync as follows:

Backbone.sync = function (method, model, options) {

    var dao = new model.dao(window.db);

    switch (method) {
        case "read":
            if (model.id)
                dao.find(model, function (data) {
                    options.success(data);
                });
            else
                dao.findAll(function (data) {
                    options.success(data);
                });
            break;
        case "create":
            dao.create(model, function (data) {
                options.success(data);
            });
            break;
        case "update":
            dao.update(model, function (data) {
                options.success(data);
            });
            break;
        case "delete":
            dao.destroy(model, function (data) {
                options.success(data);
            });
            break;
    }

};

And finally, we need to open the database (and populate it with sample data) at startup:

window.startApp = function () {
    var self = this;
    window.db = window.openDatabase("WineCellar", "1.0", "WineCellar Demo DB", 200000);
    var wineDAO = new WineDAO(self.db);
    wineDAO.populate(function () {
        this.templateLoader.load(['wine-list', 'wine-details', 'wine-list-item'], function () {
            self.app = new AppRouter();
            Backbone.history.start();
        });
    })
}

Download

I added this version of the application to the backbone-cellar GitHub repository here.

NOTE: You will have to create a PhoneGap project (see documentation here) or use the hosted build service to package the code as a native app for different mobile platforms.

11 Responses to Sample Mobile App with Backbone.js, PhoneGap, and a Local Database

  1. Dustin Sullivan February 10, 2012 at 12:02 am #

    Christophe,

    Great example here but have you attempted to run this on a mobile phone? I just tried in an emulator and android and the List does not scroll.

    • Benjamin February 21, 2012 at 11:55 am #

      Same here. Ran it on IOS, the list didn’t scroll as well. Are we missing something here?

  2. Ido Green February 14, 2012 at 7:32 am #

    Hey Christophe,
    It’s a nice example. I wonder why you are not using indexedDB in it as the W3C has officially “deprecated” Web SQL Database?

  3. line focus March 1, 2012 at 5:21 am #

    How to build group swf file single project thats run ipad any one help me

  4. Calling all Technology Professionals May 1, 2012 at 10:02 am #

    Guys check this out….. If you have ideas on mobile apps for use on phones, submit ur apps a contest that gives you a chance to prove your mettle. Whether you are a professional developer or a student, every1 can submit their apps developed by using the Series 40 Web apps SDK and win.
    Register today to submit ur apps….
    http://bit.ly/ICUrkh

  5. Junaidix June 6, 2012 at 4:21 am #

    Really cool :)

    I plan to use this technique in my mobile apps soon.

  6. Tyrone July 22, 2012 at 8:35 am #

    The github has a page not found error. please I would like to see the example.

    • Eduardo August 9, 2012 at 3:08 pm #

      I also would like to see the example if possible.

      As this is my first comment, I would like also to congratulate you for this blog. Its helping me a lot with mobile development using phonegap, and to learn about Backbone.

  7. Uri September 28, 2012 at 12:34 pm #

    Correct URL to the backbone-cellar GitHub repository:

    https://github.com/ccoenraets/backbone-cellar/tree/master/tutorial/mobile/offline

  8. abayomi October 21, 2012 at 11:58 pm #

    Hello christophe.Thank you for your blog and for taking out time to impact knowledge.However,I appreciate your employee directory with phone gap api for local storage-it has helped in my understanding of the subject.I came accross a problem recently cos I was trying to insert into my database in my local language with this text(Odò kan si ti Edeni ṣàn wá lati rin ọgbà na; lati ibẹ̀ li o gbé yà, o si di ipa ori mẹrin. ) but on display the result after select query gave was different(Odò kan si ti Edeni ṣà n wá lati rin á»gbà n…).Can you help.How do I get the exact text I inserted in the database.

  9. Farklı Yemekler July 25, 2013 at 9:51 am #

    It’s a nice example, thanks.

Leave a Reply

css.php