PhoneGap + JavascriptMVC + jQuery Mobile
I started a new mobile app project and decided to go with HTML5 wrapped with PhoneGap. The reasons are many and this post isn’t about why, but how.
Before you start, here are the libraries/frameworks I am using:
jQuery Mobile – a “Touch-Optimized Web Framework for Smartphones & Tablets”
Javascript MVC – a Javascript MVC Framework (and more)
PhoneGap – “HTML5 app platform”/embedded cross platform browser
I will be posting code snippets as I go along that may be helpful to someone just starting out with a similar project.
Here is my directory structure so far:
- blocks
- controllers
- views
- blocks.js
- pages
- controllers
- views
- models
- models.js
- blog_post.js
- css
- images
- platforms // platform projects (iOS XCode project, Android eclipse project, etc.)
- myapp.js
- myapp.html
- build.xml
I also chose to go with EJS for the templating system which allows my view to look very similar to a PHP/Zend Framework view:
<%== blocks('header') %> <div data-role="content" id="frontpageContent"> <ul data-role="listview" data-theme="c"> <% if (items) for(var i=0; i < items.length; i++) { %> <li data-icon="tcc2-arrow-right"> <a href="#"> <img src="<%= items[i].media.m %>"/> <h3><%= items[i].title %></h3> </a> </li> <% } %> </ul> </div> <%== blocks('footer') %>
The first piece of code that makes life a little easier on large projects is my blocks view helper. This way I can modularize and reuse:
$.EJS.Helpers.prototype.blocks = function (blockName) { if (typeof MyApp.Blocks[blockName] === 'function') { blockInstance = new MyApp.Blocks[blockName]($('<div/>')); return blockInstance.view; } throw "No such block controller MyApp.Blocks." + blockName; }
You could also just call
<%== $.View('someview.ejs') %>
if you have no need for a controller. Block controllers look like this:
$.Controller('MyApp.Blocks.Header', /** @Static */ { defaults: {}, pluginName: 'header' }, /** @Prototype */ { init : function(){ this.view = $.View("my_app/blocks/views/header.ejs",{}); } } ); // Header controller
Now on to more substantial jQuery Mobile related code. We need jqm navigation to tie into our MVC controllers and vice versa. In other words, when we navigate to #frontpage, we want a new page injected into our dom using the frontpage controller, which in turns will render a view (frontpage.ejs, or init.ejs if you stick to the generated naming).
The answer to this problem comes straight from the jQm manual:
From myapp.js (or whatever you called your app)
steal( 'phonegap-1.3.0.js', 'jquery/jquery.js', './css/jquery.mobile.structure-1.0.min.css' // jQuery Mobile structure CSS ) .then( './css/global.css', // Our global css style and jqm theme 'jquery/controller', 'jquery/view/ejs' ) .then( './models/models.js', // all models './pages/pages.js', // all pages './blocks/blocks.js', // all blocks and Blocks view helper function () { // Init some jqm settings and fadeIn splash page $(document).bind("mobileinit", function(){ $.mobile.touchOverflowEnabled = true; $.mobile.allowCrossDomainPages = true; }); }) .then('//library/jquery.mobile-1.0.min.js') // jQuery Mobile library .then( // Run the frontpage controller function() { $('#splash').fadeIn(500); // intercept changePage events to inject our controllers as needed (dispatch) $(document).bind( "pagebeforechange", function( e, data ) { // we only handle url changes if ( typeof data.toPage === "string" ) { // We are being asked to load a page by URL var u = $.mobile.path.parseUrl( data.toPage ), re = /^\#([a-zA-Z]+)$/; if (u.hash.search(re) !== -1 ) { var $page = re.exec(u.hash); $page = $page[1]; console.log('Loading page ' + $page); // We're being asked to display the items for a specific page // Call our internal controller if (typeof MyApp.Pages[$page] === 'function') { var controller = new MyApp.Pages[$page]( $('body') ); controller.view.done(function (htmlOutput) { $(htmlOutput).appendTo( $('body') ).page(); $.mobile.changePage( $(htmlOutput) ); }); } } } }); setTimeout(function () { $.mobile.changePage('#frontpage'); }, 500); });
This is of course a very early draft, but I hope it is enough to fill in the gaps between the docs of JMVC and jQm to get someone started. There is much you need to add to make this nicer. For example you could have an routes object defining each hash and params and which controller to send them to, then use that during the event above (look at jquery page params for adding hash params to jQuery Mobile).
Thanks for the post – very helpful. Maybe a silly question, but how did you get PhoneGap to recognize a main page other than index.html? Thanks!
It’s a modification to the native code. You’ll need to make the change to both the Java code for Android and the Objective-C code for iOS.
Here is an example for iOS (I did not write this):
https://gist.github.com/800681
You can change this:
super.loadUrl(“file:///android_asset/www/index.html”);
for this:
super.loadUrl(“file:///android_asset/www/myapp.html”);
public class MainActivity extends DroidGap
{
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.loadUrl(“file:///android_asset/www/index.html”);
}
}
great tutorial!! i would like to know more about create dynamic pages!! thank