Making An App With PhoneGap & jQm Part II: Loading JavaScript Properly 10

If you missed Part I: A Simple HTML5 Skeleton, you may want to go there and grab the code we wrote so far so that you can follow more easily what we'll do in this part.

One of the challenges in developing apps using PhoneGap and jQuery.Mobile is to make sure that the libraries are properly loaded before any critical code is executed. Another challenge is to make the difference between code which is due to execute only once, code which will execute on every activity, etc. In this part we'll explore those differences and I'll present to you a way of making sure that your functions execute at the right time.

Namespacing The Application's JavaScript

Namespacing your functions into an object is a great way of avoiding potential conflicts with existing functions; it also serves as an additional separation of your JS code from your HTML, it can make it easier to debug errors by keeping related entities close to each other. It may also make the use of a test framework like QUnit easier to implement.

Here is the base skeleton we'll use for our app:

var App = {
	"app_loaded": false,
	"testing_on_desktop": true,

	"init": function () {
	},

	"utilities": {
	},
};

A quick note before we go deeper: you can make use of the object notation in JavaScript to nest multiple namespaces, like I did with "utilities". This is a pretty cool feature to make your code a bit more organized than putting everything at the same level.

Setting Up jQuery.Mobile In <head>

As we've seen in Part 1, jQuery.Mobile is a bit special in the sense that it modifies the DOM upon loading. For this reason, any configuration code we'd like to see executed must be set before the library is loaded, using the "mobileinit" event which marks the beginning of jQuery.Mobile's execution.

As an example of what you can configure, as my app needs to "phone home" I had to activate the cors option in both jQuery and jQuery.Mobile. The full list of configuration variables is available here. Here's the result in <head>:

	<script charset="utf-8" src="js/app.js"></script>
	<script>
		jQuery(document).bind("mobileinit", function () {
			console.log("configuring jqm...");

			// required for "phoning home" (load external assets, etc.)
			jQuery.support.cors = true;
			jQuery.mobile.allowCrossDomainPages = true;
			
			jQuery.mobile.phonegapNavigationEnabled = true;
			jQuery.mobile.defaultDialogTransition = "pop";
			jQuery.mobile.defaultPageTransition = "none";
			
			jQuery.mobile.loader.prototype.options.text = "loading";
			jQuery.mobile.loader.prototype.options.textVisible = true;
			jQuery.mobile.loader.prototype.options.theme = "a";
		});
		App.init();
	</script>
	<script charset="utf-8" src="js/jquery.mobile-1.3.1.min.js"></script>

I advise you to systematically add console.log("[funcName]") at the beginning of every function as it makes debugging much much easier, especially when callbacks are involved because they can be rather messy in the way/order they fire. Oh, notice the App.init() call? Let's see what we can put in there.

Setting Up Our App In App.init

Now that we've set up jQuery.Mobile, let's list the different functions/actions which need to be performed and when they are to be executed:

When the application launches
  • Determine whether we're on a desktop browser or on a smartphone
  • Hide the application's splashscreen
When any activity (or "screen", "page", whatever) launches
  • Make sure the device is connected to the Internet
  • Determine whether the user is logged in or not
  • If not, determine whether the user is logging in/registering
  • If not, show the login form
When a specific activity launches
  • Set up callbacks on the activity's forms
  • Fetch the activity-specific data
  • Execute other activity-specific JS code

As you can see, there are three distinct levels of code execution we have to handle. On top of that, we have to make sure that both PhoneGap and jQuery.Mobile are properly loaded before executing some critical code… a real challenge!

Let's do this slowly. Take a look at the following App.init() snippet:

	"init": function () {
		console.log("[init]");

		if (document.URL.indexOf("http://") === -1) {
			App.testing_on_desktop = false;
		}

		jQuery(document).ready(function () {
			console.log("jQuery finished loading");

			if (App.testing_on_desktop) {
				console.log("PhoneGap finished loading");
				_onDeviceReady();
			} else {
				document.addEventListener("deviceReady", function () {
					console.log("PhoneGap finished loading");
					_onDeviceReady();
				}, false);
			}

			jQuery(document).one("pageinit", function () {
				console.log("jQuery.Mobile finished loading");
			});

			console.log("PhoneGap & jQuery.Mobile finished loading");
			initPages();
			console.log("App finished loading");
			App.app_loaded = true;
		});

		function _onDeviceReady () {
		};
		function initPages () {
		};
	},

Phew, that's a lot of code! Let's break it down.

First, note that we're calling App.init() from <head> which means that the code present in it will only be executed once per launch. However, we can't be sure that any of the libraries have loaded yet!

Let's immediately determine whether we're in a desktop browser or in the smartphone's browser engine. This is using a small trick, but it's really simple: when you're executing in PhoneGap (emulator & real device), the index.html file gets loaded from the filesystem and not via a server as would be the case on a desktop browser. This means that the URL won't begin with "http://" on a smartphone, which is what we check.

The call to jQuery(document).ready() allows us to be sure that jQuery has loaded completely at this point, so we'll put the remainder of our logic in this callback.

Then we have:

if (App.testing_on_desktop) {
	console.log("PhoneGap finished loading");
	_onDeviceReady();
} else {
	document.addEventListener("deviceReady", function () {
		console.log("PhoneGap finished loading");
		_onDeviceReady();
	}, false);
}

This is making use of another trick to handle the possibility that we're on a desktop browser. When PhoneGap has loaded and established a link between JavaScript and the smartphone, it fires a deviceReady event. However, when you're testing your app on Chrome, this link can obviously never be established hence deviceReady never gets fired! This snippet takes care of that by firing our callback immediately when we're on a desktop browser, and only upon linking otherwise.

The next part uses the "pageinit" event to make sure that jQuery.Mobile has loaded. Note that we can't use "mobileinit" because the event gets fired before the lib has fully loaded.

Finally, we have some code firing when everything is ready, calling initPages() to set up the code executing on every activity and setting App.app_loaded to true.

At this point, it seems that everything is going fine, but let's take a step back and think about it for a minute. Do you see a problem with the code I've shown you so far? I do!

Here's the thing: the last four lines of code require PhoneGap, jQuery and jQuery.Mobile to be fully working before they are reached, right? The problem is that we can't be sure that both deviceReady and pageinit have fired by the time we reach those lines, because those events are firing callbacks and our last four lines will be executed sequentially upon setting up the callbacks, not upon executing them. This can get pretty confusing, don't hesitate to read the last paragraph again if this isn't clear to you.

So what we need is a way to say: "hey, I want to execute function C when and only when functions A and B have executed". Fortunately, jQuery provides us with a way to do just that: jQuery.Deferred(). Here's how it works: we'll define two variables deviceReadyDeferred and jqmReadyDeferred containing special objects called Deferred objects. Those objects are considered in a "pending" state. In each of the two callbacks on "deviceReady" and "pageinit", we'll set the corresponding object's state to "resolved" using calls like deviceReadyDeferred.resolve(). Last but not least, we'll use a special callback that will fire only when both objects have resolved, ensuring that both PhoneGap and jQuery.Mobile have loaded completely. Here's the modified code:

		jQuery(document).ready(function () {
			console.log("jQuery finished loading");

			var deviceReadyDeferred = jQuery.Deferred();
			var jqmReadyDeferred    = jQuery.Deferred();
			if (App.testing_on_desktop) {
				console.log("PhoneGap finished loading");
				_onDeviceReady();
				deviceReadyDeferred.resolve();
			} else {
				document.addEventListener("deviceReady", function () {
					console.log("PhoneGap finished loading");
					_onDeviceReady();
					deviceReadyDeferred.resolve();
				}, false);
			}

			jQuery(document).one("pageinit", function () {
				console.log("jQuery.Mobile finished loading");
				jqmReadyDeferred.resolve();
			});

			jQuery.when(deviceReadyDeferred, jqmReadyDeferred).then(function () {
				console.log("PhoneGap & jQuery.Mobile finished loading");
				initPages();
				console.log("App finished loading");
				App.app_loaded = true;
			});
		});

Now if you load your page in a desktop browser and open the console, you should see all the console.log() calls we've places ocurring in the right order:

  • [initApp]
  • configuring jqm…
  • jQuery finished loading
  • PhoneGap finished loading
  • [onDeviceReady]
  • jQueryMobile finished loading
  • PhoneGap & jQueryMobile finished loading
  • [initPages]
  • App finished loading

Pretty cool, isn't it? Now all we have to do is add some code in App.init():

	function _onDeviceReady () {
		PGproxy.navigator.splashscreen.hide();
	};
	function initPages": function () {
		console.log("[initPages]");
		jQuery(document).bind("pageinit", _initPages);
		
		function _initPages () {
		};
	};

Our initPages() sets up a callback which will be fired on every page load. This callback will contain code to check the login/registration status and maybe show logged out users a login box. As for onDeviceReady(), it hides the application's splashscreen. Now you may wonder where this PGproxy object comes from. I'll explain, don't worry.

Introducing PhoneGap Proxy "PGproxy"

One of the challenges I faced whan I began coding was that the smartphone native functions, which are made available to JavaScript by PhoneGap, weren't available when testing the app in Chrome (obviously). Now there are ways to avoid errors in Cordova by testing systematically that the functions exist before executing them, like this:

if (navigator.notification && navigator.notification.vibrate) {
	navigator.notification.vibrate(a);
} else {
	console.log("navigator.notification.vibrate");
}

…but as you can see it's kind of polluting our space with lines of code. To avoid this or at least hide the checks away from my functions, I added a small mock object called "PGproxy", which handles both cases (testing on Chrome or using from emulator/app) silently:

// emulate PhoneGap for testing on Chrome
var PGproxy = {
	"navigator": {
		"connection": function () {
			if (navigator.connection) {
				return navigator.connection;
			} else {
				console.log('navigator.connection');
				return {
					"type":"WIFI" // Avoids errors on Chrome
				};
			}
		},
		"notification": {
			"vibrate": function (a) {
				if (navigator.notification && navigator.notification.vibrate) {
					navigator.notification.vibrate(a);
				} else {
					console.log("navigator.notification.vibrate");
				}
			},
			"alert": function (a, b, c, d) {
				if (navigator.notification && navigator.notification.alert) {
					navigator.notification.alert(a, b, c, d);
				} else {
					console.log("navigator.notification.alert");
					alert(a);
				}
			}
		},
		"splashscreen": {
			"hide": function () {
				if (navigator.splashscreen) {
					navigator.splashscreen.hide();
				} else {
					console.log('navigator.splashscreen.hide');
				}
			}
		}
	}
};

Here is how it works: for each PhoneGap API function you use in your app, mock it once in PGproxy using the same kind of checks I used in the example code snippet. All you then have to do for using navigator.notification.alert() is to prefix it with PGproxy. If we're testing on Chrome and the Cordova library isn't available, then the proxy will fall back to native functions when possible (say for alerts) and fail silently with a console.log() trace otherwise. Usage example:

"myfunction": function () {
	// code
	PGproxy.navigator.notification.alert("message", callback, "title", "buttons");
	// code
}

Wrapping Up With Activity-Specific Code

Now we've successfully coded a way to execute our code at two levels out of three, the remaining one being on specific activities. To accomplish this last requirement, we'll use a "pageshow" event which we'll attach to those specific activities, directly in index.html:

	<div data-role="page" id="ActivityFoo">
		<div data-role="header">
			<h1>Header 1</h1>
			<div class="topmenu" data-role="controlgroup" data-type="horizontal">
				<a class="current-page-item" href="index.html#ActivityFoo" title="Foo" data-role="button"><i class="fontawesome icon-home"></i></a>
				<a href="index.html#ActivityBar" title="Bar" data-role="button"><i class="fontawesome icon-ticket"></i></a>
			</div>
		</div><!-- /header -->
		<div data-role="content">
			<h1>Content 1</h1>
		</div><!-- /content -->
		<div data-role="footer">
			<h1>Footer 1</h1>
		</div><!-- /footer -->
		<script>
			jQuery("#ActivityFoo").on("pageshow", function(e) {
				// foo
			});
		</script>
	</div>

I know this isn't necessarily the most beautiful way of doing things, but:

  • It's dead simple
  • It works wonderfully
  • It's really flexible: you can add an activity and its specific code right away
  • It's clean enough in the sense that you can always put the callback away and simply point to it from index.html

As far as I'm concerned, all those advantages far outweigh the fact that we've been obliged to put javascript in our document. If you have a better way to do such a thing, please tell me in the comments section!

All right, we're finished for now. The next step is Part III of this tutorial. UPDATE: It's online! I'll talk about authentication, which is a must-have is many mobile applications. In the meantime, happy coding!

Changes:

  • 2013-08: Changed how callbacks are handled to make the code clearer.

10 thoughts on “Making An App With PhoneGap & jQm Part II: Loading JavaScript Properly

  1. Reply Erich Gruttner Jun 15,2013 4:34 pm

    Great tutorial!!!
    Please keep up the good work.
    Question:
    Do you know how to put a loading screen in iOS? I'm using navigator.notification.activityStart(), but it only works on Android.
    Thank you in advance.

    Best regards!

  2. Reply Simon Aug 10,2013 6:56 pm

    Nice setup!
    Very clean and lots of stuff in one place. Hard to find a decent source as the info is so scattered around the net…
    Would love to see part III soon, as I am already building a sample solution implementing your aproach.

    All the best!

  3. Reply Thomas Aug 11,2013 12:22 am

    Thanks a lot for your comment, Simon! Part 3 about statelessness is live at this location; tell me what you think about it πŸ™‚

  4. Reply martin Aug 26,2013 3:16 pm

    pretty nice, exactly what I was searching for…similar discussion (http://stackoverflow.com/questions/10945643/correct-way-of-using-jquery-mobile-phonegap-together) but yours is better πŸ˜‰ thx

  5. Reply Thomas Aug 26,2013 6:28 pm

    Thanks for the kind words, martin πŸ™‚ Anything else you'd like me to write about?

  6. Reply netalex Feb 25,2014 11:39 am

    Erich Gruttner, if you mean a splashscreen there's the specific core plugin in phonegap.

  7. Reply Diptesh Shrestha Apr 16,2014 4:55 pm

    Hi,

    Thanks, its a great tutorial. I noticed that you mentioned following:

    ———————————-
    function initPages": function () {
    console.log("[initPages]");
    jQuery(document).bind("pageinit", _initPages);

    function _initPages () {
    };
    };
    ———————————-

    As the above syntax does not seem proper. Do you mean something like this?

    ———————————-
    function initPages () {
    console.log("[initPages]");
    jQuery(document).bind("pageinit", _initPages);
    };

    function _initPages () {
    };
    ———————————-

  8. Reply Diptesh Shrestha Apr 16,2014 6:05 pm

    Hi,
    I ran into one more problem. When i set "jQuery.mobile.phonegapNavigationEnabled = true;", as you mentioned, I started getting problem as the back navigation stopped working,in console i can see the following:
    —————————————-

    missing exec:App.backHistory

    TypeError: undefined is not a function

    —————————————–
    If I set "jQuery.mobile.phonegapNavigationEnabled = false;" then it again works properly. Any explanation on this (if possible)?

    Also,
    Thanks in advance.

  9. Reply Utz Zimmermann Apr 10,2017 4:00 pm

    Hello, you are the greatest – i love your post.
    After searching around for days I finally found your site.
    I thank you very much in particular for the part with jQuery.Deferred ()! I had a headache while reading the corresponding chapter on the jquery page. I am now confident that with my small app I could finally solve the loading problem with the pageready event. Thank you very much for this excellent tutorial.
    Also i like the style of your page. It’s a nice combination of fonts. My congratulations.

Leave a Reply

  

  

  

Time limit is exhausted. Please reload the CAPTCHA.