Creating Mobile Apps With KnockOutJS, PhoneGap & jQuery Mobile

In part 4 we built a complete working version of the Savings Goal Simulator as a rich web app where each view model, view and view mediator is nicely modularized. How could we leverage our browser-side markup and logic to create a mobile app , portable across multiple platforms such as IOS, Android, Windows Mobile? Well, the goal of this post is to present a potential approach. (Of course you can take a peek at the “So What?” section)

Mobile App Framework Stack

The idea behind PhoneGap is to provide a cross-mobile-platform framework allowing a self-contained web app built using HTML5 + CSS3 + Javascript to run inside a full screen web view , while allowing access to the device features such as for example: geolocation, accelerometer, camera, storage, etc..

So PhoneGap will serve as our foundation. For each platform you intend on supporting, you will use the corresponding SDK, code project templates and PhoneGap runtime. Let’s take a look at the 3 most common platforms:

On IOS, the SDK and application consist of:

  • a CordovaLib libray
  • an XCode project template
  • a main module launching a UIApplicationMain
  • an AppDelegate class launching a ViewController
  • a ViewController loading the main page (index.html) of your app into a WebView

On Android, the SDK and application consist of:

  • an Apache Cordova JAR containing the Java implementation of PhoneGap for the Android platform
  • an Eclipse project template
  • a DroidGap parent class used for your own Java Droid activity class loading the main page (index.html) of your app.
  • an assets/www folder containing web resources (.html, .js, .css, images, etc.)

On Windows Mobile Phone, the SDK and application consist of:

  • a WP7CordovaClassLib library
  • a XAML application with a phone application page containing a CordovaView
  • a CordovaView including a browser window hosting the main page (index.html) of your app
  • a www folder containing web resources (.html, .js, .css, images, etc.)

In summary, for each platform, PhoneGap is used to create a sort of “ launcher ” application, which starts a “ controller “, which in turns loads your main .html web page into a full-screen web view , and exposes underlying platform features through a Javascript API .

PhoneGap Framework

So with PhoneGap, we will keep the launcher part of our mobile app fairly small and trivial. Consequently the minimum skills and associated learning curve for each platform we’ll want to target will also be more manageable. The majority of your app components will be based on standard HTML5 + CSS3 + Javascript.

The approach will consist of:

  1. Creating (or leveraging parts of) a minimal web app using HTML and Javascript
  2. Hosting the web app in a PhoneGap “ launcher ” app (based on the desired mobile platform)
  3. Leveraging jQuery Mobile UI widgets for a consistent look and feel
  4. Theme the app using CSS3 including transitions
  5. Expand the web app functionality while:
    • Modularizing pages and views using a templating engine
    • Partitioning application logic between view models and view mediators
    • Using KnockoutJS for data-binding

Your starting point may differ based on your current skills and experience:

  • If you already have a significant development experience on a given platform (e.g. Mac OSX with XCode , or Windows with Visual Studio ), then download the SDK for your platform and follow the corresponding Getting Started Guide . You will benefit from a more integrated experience and faster device emulation.

    Note for IOS: an Apple Developer License will be required if you want to deploy the app to your device (otherwise you will only be able to use the emulator).

  • If you have limited overall experience, starting out with the Android platform and the Aptana Studio IDE (a custom version of the Eclipse IDE optimized for HTML5/CSS3 and Javascript development) might be easier as you can run it on any OS (Windows, Mac, Linux). To get started you will need to install the following tools (in order):

    Note: I recommend the basic tutorial to create a basic Android app to ensure all the prerequisites are installed correctly. Then you can follow the PhoneGap Getting Started Guide for Android .

In the rest of this tutorial I will assume that your mobile development environment is configured and working, and that you have successfully followed the PhoneGap Getting Started Guide. In the next sections I will use the Android platform as an example.

Note: this tutorial builds upon the previous four tutorials and assumes you have built a rich interactive web application using view models, view mediators, and modularized views.

Before we get started, here is a bird’s-eye-view of the steps we will be going through in details in later sections:

  1. Creating the actual app shell using PhoneGap’s Getting Started Guide for the platform you want to start out with.
  2. Test the basic sample shell to make sure everything is working correctly
  3. Customize the assets/www folder structure including all needed JS libraries
  4. Add all needed references for CSS and Javascript files
  5. Define placeholder / container divs for each “logical” jQuery Mobile page
  6. Create separate sets of files for each logical page:
    • view markup (.html) based on a templating engine
    • view model code (.js)
    • view mediator code (.js)
  7. Create a main application Javascript module responsible for
    • loading all view markup / templates
    • initialize all view mediators (wich in turn initialize their respective view model and data bind the view model to the view)
    • trigger the overall rendering of the main page by jQuery Mobile
  8. Create a phone and a tablet version of the main web page (e.g. index.phone.html vs. index.tablet.html
  9. Customize the mobile controller to load the appropriate index page based on device type (phone vs. tablet)
  10. Tailor the look and feel (layout, styling, etc.) for the tablet vs. phone version
  11. Prepare for packaging
  12. Host your app on an “app store”

Building Our App Step-By-Step

Once we are finished the mobile version of the Savings Goal Simulator will look like this:
New Look

Let’s download the version of PhoneGap from http://phonegap.com/download and extract the content to a PhoneGap folder (e.g. PhoneGapSDK). We’ll use the content later.

Creating The Android Application Shell

On the Android platform, the application shell takes the form a Java class called an “ activity “. It inherits from the android.app.Activity which is the mobile app’s main application controllerin the Model-View-Controller sense.

So let’s get started in Aptana Studio and follow the steps from the PhoneGap Getting Started With Android guide :

  1. From Aptana Studio, select File, New Project to start the project creation wizard
  2. Expand the Android folder and select “Android Application Project”
  3. Let’s name the application and Eclipse project “Savings Goal Simulator”
  4. Customize the namespace e.g.: com.yournamespace.savingsgoalsimulator
  5. Select the minimum version of the Android SDK your app should require, e.g. API7: Android 2.1
  6. By default the project folder will be created in the default Eclipse workspace project unless you want to customize the path.
    Creating the base Android app
  7. Click Next
  8. Accept the default icon settings for now (you can customize them later) and click Next
  9. Accept the default kind of activity settings and click Next
  10. Accept the default activity settings and click Next – the project will then be created:
    Creating the base Android app

At this point we have a basic Android app that you can run in the simulator:

  • Select Run, then Run,
  • Select “Android Application” in the Run As dialog, then click Ok
  • The emulator will boot Android
  • Then click and drag the lock to the right to unlock the virtual phone, your app will appear:

    Running the base Android app

Let’s take a brief look at what was generated:

  1. Expand the src folder recursively until you can see and double-click on the MainActivity.java file.

    package com.yournamespace.savingsgoalsimulator;
    import android.os.Bundle;
    import android.app.Activity;
    import android.view.Menu;  
    
    public class MainActivity extends Activity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }   
    
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            getMenuInflater().inflate(R.menu.activity_main, menu);
            return true;
        }
    }

    }

  2. The MainActivity class inherits from the core Android Activity class
  3. The OnCreate method calls setContentView to create the main view based on the layout provided
  4. Under the res\layout folder, open the activity_main.xml resource
  5. The RelativeLayout includes a TextView for which the text is provided by the string resource named hello_world
  6. Under res\values , open the strings.xml resource and find the definition of hello_world

Once we upgrade our app to PhoneGap, we will no longer use an Android content view nor a layout. Instead we will use a web view loaded with a local web page.

Creating The PhoneGap Application Shell

PhoneGap provides a subclass of Activity called DroidGap which instead of creating an Android content view based on a layout, will create a web view and load the .html page of your choice , which like any page will load stylesheets and scripts, and run the Javascript code. So we’ll make the necessary changes to upgrade the default app to PhoneGap:

  1. Let’s copy the Cordova Java archive ( cordova-2.2.0.jar as of this writing) of the lib\android located in our PhoneGapSDK folder we extracted earlier to the libs folder of your project (the folder currently contains a file named android-support-v4.jar). The Cordova JAR will now appear in Aptana Studio under libs .
    Adding the Cordova JAR to the project

  2. Now we need to add the Cordova JAR to the Java Build Path. In AptanaStudio, right click on the libs folder, select “Build Path”, then select “Configure Build Path”, select the “Libraries” tab, click on “Add JARs …”, drill down to the libs folder of your project, select the Cordova JAR , and click OK.
    Adding the Cordova JAR to the Java Build Path

  3. The Cordova JAR will also appear under the “ Referenced Libraries ” folder of your project.
    Java Build Path - Referenced Libraries

  4. Let’s go to the MainActivity Java class source file. Since we’re no longer going to need the default Android Activity approach, let’s replace the 2 imports related to android.app.Activity and android.view.Menu by a import for the org.apache.cordova.* namespace:

    package com.yournamespace.savingsgoalsimulator;
    import android.os.Bundle;
    import org.apache.cordova.*
  5. Let’s change the parent class for MainActivity to the new DroidGap class:

    public class MainActivity extends DroidGap
  6. Now we’ll change the implementation of the onCreate method. So let’s replace the line starting with setContentView by the following code which load the content from a local url:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.loadUrl("file:///android_asset/www/index.html");
    }
  7. Let’s remove the onCreateOptionsMenu method as it is no longer needed.

  8. If we ran the app again, the emulator would tell us that the index.html could not be loaded which makes sense since we have not created it yet!
    No index.html found yet

  9. Note that PhoneGap is looking for the resource in a www folder under a logical android asset folder. Although it might seem counter-intuitive Android maps the “ android asset ” url folder to the physical assets folder! So right click on assets and create the www folder.

  10. Under assets/www , create an index.html web page file with the following basic HTML source:

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>Cordova</title>
        </head>
        <body>
            <h1>Hello World</h1>
        </body>
    </html>
  11. Re-run the app and the page should now display in the emulator. At this point we just exercized the basic ability for the DroidGap class to load a web page into a web view. We’ll soon add the Cordova Javascript library to the page so we can expose the bi-directional API between the web page and the actual device. But first we need to add the permissions PhoneGap will need to the application. For that, double-click on the AndroidManifest.xml and switch to the XML view. Locate the line starting with the <application tag and insert the following lines above it, then save the XML file:

    <supports-screens 
        android:largeScreens="true" 
        android:normalScreens="true" 
        android:smallScreens="true" 
        android:resizeable="true" 
        android:anyDensity="true" />
    
    
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.BROADCAST_STICKY" />

    Technically your application may only need a subset of these permissions, but for this tutorial we’ll add them all. Additionally, if you want the application to support orientation changes, add the following attribute before the end of the <activity tag !(located underneath the <application tag):

    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale"

    The whole opening <activity tag should now look as follows:

    <activity
        android:name=".MainActivity"
        android:label="@string/title_activity_main"
        android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale"
     >
  12. To allow our WebView to interact with the PhoneGap / Cordova plugins for the mobile device we need to add configuration information to our res resource folder. So copy the xml folder located in the android folder of the PhoneGapSDK folder to the res folder of our project:
    Adding the Cordova xml folder to the project

    Note: if you forget this step, later the mobile app will show an error message indicating that the Cordova class cannot be found since the WebView will not be able to locate the Cordova plugins.

  13. As in our original Creating Rich Interactive Web Apps With KnockOut.js – Part 2 tutorial, let’s further expand the www folder into subfolders, by creating a scripts subfolder, and below that a vendor subfolder. Now go back to the PhoneGap SDK folder and copy the Cordova Javascript library file (cordova-2.2.0.js as of this writing) to our new assets/www/scripts/vendor folder.
    Adding Cordova JS to the project

  14. Let’s create a separate application.js shell module under our assets/www/scripts folder. The module will contain the code to initialize our web page but for now, let’s keep it minimal and let’s provide 2 functions: InitializeApplication and OnLoad:

    function InitializeApplication() {
        alert("Application Initialized!");
    }  
    
    
    function OnLoad() {
        InitializeApplication();
    }
  15. Let’s switch to the index.html page and add our 2 Javascript files to our <head> tag:

    <head>
        <title>Cordova</title>
        <script src="scripts/vendor/cordova-2.2.0.js"></script>
        <script src="scripts/application.js"></script> 
    </head>

    And for now, let’s add a call to the OnLoad function to the body onload event of the page:

    <body onload="OnLoad();">
        <h1>Hello World</h1>
    </body>
  16. Now you should be able to re-run the application and see the alert pop-up.

    Note: if you don’t see the alert, verify the scripts and the page. You can also troubleshoot the index.html from your favorite browser debugging tools (e.g. Chrome, Developer Tools). This is always the best technique to troubleshoot your Javascript anyway.

  17. We’re now ready to dig into the application initialization lifecycle! The browser triggered the onload event once the page has been loaded. PhoneGap / Cordova will trigger a different event named deviceready once the bidirectional API between the mobile device and Javascript has been fully initialized. To react to the deviceready event we will need to add an event listener once the onload event has been triggered. So let’s add a new function named OnDeviceReady to our application.js module. We’ll move the call to InitializeApplication from OnLoad to the new function since we actually want to initialize our app once the deviceready event has occurred.

    function OnDeviceReady() {  
        InitializeApplication();
    }

    Now let’s setup the event listener in the OnLoad function:

    function OnLoad() {
        document.addEventListener("deviceready", OnDeviceReady, false);
    }

    This allows us to customize the InitializeApplication to use some of the device information exposed by PhoneGap:

    function InitializeApplication() {
        alert("Welcome to PhoneGap " 
                + device.cordova 
                + " version " + device.version 
                + " for " + device.platform 
                + " on device: " + device.name );
    }

    Now let’s run the application again and our alert with the device information should appear!
    Application Initialization On deviceready Event

This concludes the creation of the basic PhoneGap application shell. In summary here is what our application does:

  1. Android launches our MainActivity class which inherits from the DroidGap
  2. The onCreate method of DroidGap creates a WebView
  3. The onCreate method of MainActivity sets the url content of the WebView to index.html
  4. Our index.html loads the Cordova library and our application.js
  5. The onload event of the page sets up an event listener for the PhoneGap deviceready event
  6. Once PhoneGap is fully initialized the deviceready event is triggered
  7. Our custom InitializeApplication function is called – at that point the PhoneGap API is fully operational

Applying a jQuery Mobile “Veneer”

Introduction to jQuery Mobile

Since our app is built using HTML and CSS we can create a complete custom look and feel for our app. This is fine if you are creating something truly unique like a game or something very graphical.
But if you are planning on creating an application focused on manipulating data I would recommend following mobile UI conventions . One easy way to ensure a more standardized look that your customers will feel familiar with is to leverage jQuery Mobile. You will benefit from:

  • a set of common UI elements optimized for a mobile experience
    • larger buttons and easier to use form controls
    • toolbars, listviews, collapsible panels, grids
  • navigation between “logical pages”
  • cross-platform compatibility helping you create a uniform experience across devices
  • customizable themes

Here is how jQuery Mobile makes this possible:

  • jQuery Mobile allows declaration of its custom UI elements by augmenting traditional HTML elements with new attributes. E.g.: a jQM button is created by enhancing the anchor tag with a data-role=”button” attribute (as well as other attributes to add details about how the button should be rendered, such as with an icon positioned above the label):

    <a href="#addContact" 
        data-role="button" 
        data-icon="plus" 
        data-iconpos="top">Add Contact</a>
  • As the DOM is loaded, jQuery Mobile injects additional needed elements and CSS classes based on the various data-* attributes to produce the desired effect.

I would not be providing a balanced view point if I did not mention the following associated challenges:

  • Since “logical pages” are defined using div elements enhanced with data-role=”page” attribute which causes the physical .html page to become large and difficult to maintain:

    <div id="page1" data-role="page">
        <!-- ... -->
    </div>
    
    
    <div id="page2" data-role="page">
        <!-- ... -->
    </div>
    
    
    <!-- ... -->
    
    
    <div id="pageN" data-role="page">
        <!-- ... -->
    </div>

    We’ll use external views based on templates and the jQuery View Loader plugin to address this issue.

  • Defining common elements such as the bottom toolbar for each “logical page” also requires the use of templates if we do not want to duplicate the markup over and over.

  • Since the markup is automatically enhanced as it is being loaded, dynamically changing elements becomes a bit more complicated since jQuery Mobile needs to be notified to “re-enhance” the new markup fragments. This will become even more apparent when we use a viewmodel to view data-binding library like KnockoutJS. But again there are some solutions.

Overall despite the learning curve and the need to make jQuery Mobile “play nice” with other libraries manipulating the DOM, the benefits strongly outweigh the challenges. This tutorial will provide the necessary guidance to increase your chances of success.

Adding A jQuery Mobile Shell
  1. First, download the full ZIP version of jQueryMobile and extract it out locally.

  2. Before we add the files to our project, let’s create a css folder under assets/www . This will provide a central place for all CSS files needed for our application.

  3. Let’s create a jquery.mobile folder under assets/www/css

  4. Now copy the .css files (only) and the images folder from your extracted jQueryMobile ZIP to assets/www/css/jquery.mobile . Your project structure should now look like this:
    jQuery Mobile CSS Folder Setup

  5. And then copy the .js files from your extracted jQueryMobile ZIP to assets/www/scripts/vendor

  6. Since jQuery Mobile builds on top of the jQuery Core, download both the development and production versions of jQuery and copy the corresponding .js files to assets/www/scripts/vendor . Your project structure should now look like this:
    jQuery Mobile JS Folder Setup

  7. Now let’s go back to our index.html page and add the jQuery Mobile stylesheets. We’ll also add a meta tag to provide device information which will be leveraged by jQuery Mobile to optimize rendering for the device.

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />                
    <link href="css/jquery.mobile/jquery.mobile-1.2.0.css" rel="stylesheet" type="text/css"  />  
    <link href="css/jquery.mobile/jquery.mobile.structure-1.2.0.css" rel="stylesheet" type="text/css"  />
  8. Let’s include the .js files for jQuery and jQuery Mobile in between the cordova JS and our application.js

    <script src="scripts/vendor/cordova-2.2.0.js"></script>     
    <script src="scripts/vendor/jquery-1.8.2.js"></script> 
    <script src="scripts/vendor/jquery.mobile-1.2.0.js"></script>       
    <script src="scripts/application.js"></script>
  9. And let’s define a minimal jQuery Mobile “ logical page

    <body onload="OnLoad();">
        <div id="page-home" data-role="page">
            <div data-role="header">
                Savings Goal Simulator
            </div>
            <div data-role="content">
                Welcome!
            </div>
            <div data-role="footer">
                powered by PhoneGap and jQueryMobile
            </div>
        </div>
    </body>

    Let’s run the app in the simulator:
    Minimal jQuery Mobile Logical Page

  10. jQueryMobile fires an event named mobileinit as soon as it starts executing. Since we may want to override some of the framework defaults (more on that later), let’s add a script with an event handler for mobileinit . Since we want to setup the event handler before jQuery Mobile starts to load, we’ll define the script right before the inclusion of jQuery Mobile.

    <script type="text/javascript">
        // IMPORTANT: This must execute before jQueryMobile is loaded!
        $(document).bind("mobileinit", function() {
             console.log("mobileinit","info");
             // Placeholder for framework settings overrides
             // See http://jquerymobile.com/demos/1.2.0/docs/api/globalconfig.html
        });            
    </script>       
    <script src="scripts/vendor/jquery.mobile-1.2.0.js"></script>
  11. In the jQuery world, a ready event is triggered on the document once the page has fully loaded. But in jQuery Mobile, since you can have multiple “logical pages”, jQuery Mobile will trigger a pageinit event on a given page once it has been created and initialized. So typically we’ll setup an event handler for pageinit when triggered on the main / “home” logical page. This handler will perform any initialization specific to that page. So let’s add the following script below our “logical page” definition:

    <script type="text/javascript">
        $("#page-home").live("pageinit", function() {
            // When ALL scripts have been loaded AND executed:
            alert("page-home is ready!");
        })
    </script>

    Now run the application. What do you notice?
    The “page-home is ready!” alert message appears BEFORE the PhoneGap alert message!
    Why is that?
    Well, jQuery Mobile starts processing the markup as soon as it is created while PhoneGap deviceready event is not triggered until the whole physical page is loaded and until the PhoneGap library is fully initialized.

    IMPORTANT: The initialization code for our application will need to be split according to when various initializations are needed. For example:

    • If we need to fetch data from a remote app, we should trigger the fetch from a [jQuery Mobile] pageinit event
    • If we need to use data from or related to the device (e.g. local storage), we should retrieve the data from the deviceready [PhoneGap] event

So at this point, we have a minimal PhoneGap + jQuery Mobile application!

Transforming Our Savings Goal Simulator Web App Into A Mobile App

At the end of Creating Rich Interactive Web Apps With KnockoutJS – Part 4 we had a very modular browser-side rich web app. As you will see we’ll be able to adapt the code to “make it fit” with the PhoneGap and jQuery Mobile framework. At a high-level we will need to:

  • adapt our views to fit mobile device dimensions
  • enhance our view markup with jQuery Mobile attributes (e.g.: data-role, etc.)
Installing The Web Version Of The Savings Goal Simulator

So let’s first migrate the source of the web version of the Savings Goal Simulator to our mobile project:

  1. Download the source code for the web version of the Savings Goal Simulator from GitHub
  2. Extract and open the download source
  3. Copy the application.css from savings-goal-simulator-master\css\ to the assets\www\css folder of your project
  4. Copy only the subfolders of savings-goal-simulator-master\scripts\ to the assets\www\scripts folder of your project (do not copy the application.js)
  5. Rename the index.html in savings-goal-simulator-master to index.web.html
  6. Copy index.web.html from savings-goal-simulator-master to assets\www
  7. Rename the application.js in savings-goal-simulator-master to application.web.js
  8. Copy application.web.js in savings-goal-simulator-master to assets\www\scripts
  9. So far your project workspace should now reflect the new folders under assets\www\scripts and the new files:
    Workspace with web version of the Savings Goal Simulator
  10. To make troubleshooting easier, let’s make a couple changes so that we can run the web version using index.web.html in a browser. This way we can always compare our mobile version to the web version to help identify and resolve potential issues.
    • Open index.web.html, look for a reference to application.js and rename it to application.web.js (since we renamed that Javascript file earlier)
    • Open Google Chrome (or your favorite browser), and open the Developer Tools
    • Drag index.web.html to the browser
    • Test the app – it should work fine – you will notice that there is no “styling” since we did not copy the jQuery UI theme (because we will use jQuery Mobile instead)

Now we are ready to adapt our web app into a mobile app.

Rudimentary Migration Of The Savings Goal Simulator Source Code

The idea is to progressively enhance the basic index.html we created in the “Adding A jQuery Mobile Shell” section by incorporating elements of the web version of the app. The modest goal for this section is to be able to bring up the Savings Goal Simulator in the mobile device simulator.

  1. In Creating Rich Interactive Web Apps With KnockoutJS – Part 2 , we implemented asynchronous parallel loading of Javascript libraries using LAB.js to speed up the loading process of our web application. We will apply the same technique to our mobile app.

    • In index.html, let’s load the LAB.js Javascript library after loading application.js and let’s add a script block:

      <script src="scripts/vendor/cordova-2.2.0.js"></script>     
      <script src="scripts/vendor/jquery-1.8.2.js"></script> 
      <script src="scripts/vendor/jquery.mobile-1.2.0.js"></script>       
      <script src="scripts/application.js"></script>  
      <script src="scripts/vendor/LAB.min.js" type="text/javascript" ></script>
      <script type="text/javascript">
          $LAB
              .wait(function () {
              });
      </script>
    • Now let’s replace the script loading for jquery.mobile-1.2.0.js by a call to load the script via LAB.js:

      <script src="scripts/vendor/LAB.min.js" type="text/javascript" ></script>
      <script type="text/javascript">
          $LAB
              .script("scripts/vendor/jquery.mobile-1.2.0.js").wait()
              .wait(function () {
              // When ALL scripts have been loaded AND executed:                  
              });
      </script>
    • If we try to run our app we will notice that the “page-home is ready!” alert never pops-up and that we see an “$ undefined” error in the console. This is due to the fact that our scripts are being loaded in parallel of our page execution. So the code we had pageinit event handler we had placed below our page definition needs to be synchronized with our LAB.js-based script loading:

      <script type="text/javascript">
          $LAB
          .wait(function () {
              $("#page-home").live("pageinit", function() {
                  // When ALL scripts have been loaded AND executed:
                  //alert("page-home is ready!");
                  console.log("page-home is ready!","info");
              })
          })
          .script("scripts/vendor/jquery.mobile-1.2.0.js")
          .wait(function () {
              // When ALL scripts have been loaded AND executed:                  
          });
      </script>

      Now the application should work correctly in the simulator.

    • We can proceed and add requests to load all other Javascript libraries we’ll need:

      <script type="text/javascript">
          $LAB
          .wait(function () {
              $("#page-home").live("pageinit", function() {
                  // When ALL scripts have been loaded AND executed:
                  alert("page-home is ready!");
                  console.log("page-home is ready!","info");
              })
          })
          .wait(function (){
              // IMPORTANT: This must execute before jQueryMobile is loaded!
              $(document).bind("mobileinit", function() {
                   console.log("mobileinit","info");
                   // Placeholder for framework settings overrides
                   $.support.cors = true;
              });            
          })
          .script("scripts/vendor/jquery.tmpl.min.js").wait()
          .script("scripts/vendor/knockout-latest.debug.js").wait()
          .script("scripts/vendor/jquery.viewloader.js").wait()
          .script("scripts/vendor/jquery.cookie.js")
          .script("scripts/vendor/jquery.blockUI.js")
          .script("scripts/vendor/jquery.hotkeys.js")
          .script("scripts/vendor/jquery.maskedinput-1.3.min.js")
          .script("scripts/vendor/jquery.validate.min.js")
          .script("scripts/vendor/accounting.min.js")
          .script("scripts/vendor/jstorage.min.js")
          .script("scripts/vendor/currency-mask-0.5.0-min.js")
          .script("scripts/viewmodel/sgs.model.common.js")
          .script("scripts/viewmediator/sgs.mediator.common.js")
          .script("scripts/viewmodel/sgs.model.savings-goal.js").wait()
          .script("scripts/viewmediator/sgs.mediator.savings-goal.js")
          .script("scripts/viewmodel/sgs.model.coffee-pricing.js").wait()
          .script("scripts/viewmediator/sgs.mediator.coffee-pricing.js")
          .script("scripts/viewmodel/sgs.model.coffee-consumption.js").wait()
          .script("scripts/viewmodel/sgs.model.consumption-scenarios.js").wait()
          .script("scripts/viewmediator/sgs.mediator.consumption-scenarios.js")
          .script("scripts/viewmodel/sgs.model.savings-forecast.js").wait()
          .script("scripts/viewmediator/sgs.mediator.savings-forecast.js")
          .script("scripts/vendor/jquery.mobile-1.2.0.js")
          .wait(function () {
              // When ALL scripts have been loaded AND executed:                  
          });
      </script>

      If you run the app in the simulator or the browser you will see in the console that each of the module is being loaded.

  2. Currently the body of our index.html page has a div with a data-role of “ content “, let’s replace our “Welcome!” text in the div with the content of body tag from our index.web.html:

    <body onload="OnLoad();">
        <div id="page-home" data-role="page">
            <div data-role="header">
                Savings Goal Simulator
            </div>
            <div data-role="content">
                <script id='savings-goal-view-template'
                        type='text/html'
                        src='scripts/view/index/_savings-goal.view.html' >
                </script>
                <div id='savings-goal-view-container'
                        data-bind='template: { name: "savings-goal-view-template", afterRender: sgs.mediator.savingsgoal.setupViewDataBindings } ' >
                </div>                  
                <script id='consumption-scenarios-view-template'
                        type='text/html'
                        src='scripts/view/index/_consumption-scenarios.view.html' >
                </script>
                <div id='consumption-scenarios-view-container'
                        data-bind='template: { name: "consumption-scenarios-view-template", afterRender: sgs.mediator.consumptionscenarios.setupViewDataBindings } ' >
                </div>                  
                <script id='savings-forecast-view-template'
                        type='text/html'
                        src='scripts/view/index/_savings-forecast.view.html' >
                </script>
                <div id='savings-forecast-view-container'
                        data-bind='template: { name: "savings-forecast-view-template", afterRender: sgs.mediator.savingsforecast.setupViewDataBindings } ' >
                </div>
            </div>
            <div data-role="footer">
                powered by PhoneGap and jQueryMobile
            </div>                          
        </div>      
    </body>

    As a brief reminder, the scripts of type text/html represent definition of jQuery Templates used to render views / panels within the app using data-bind tags. So the data-role=”content” div contains 3 views to track the savings goal, scenarios, and corresponding forecast. If we run the app now the views will not appear as we have not yet ported over the view loading logic.

  3. In the web version, the view loading is done in the InitializeApplication function of application.web.js. Let’s copy the content of application.web.js at the end of application.js and rename the InitializeApplication we just copied to LoadApplicationViews .

  4. Now we need to make the call to LoadApplicationViews before jQuery Mobile gets a chance to run. So let’s insert a wait block before loading it using LAB.js:

    <script type="text/javascript">
        $LAB
        // ... shortened for brevity
        // ...
        .script("scripts/viewmediator/sgs.mediator.savings-forecast.js")
        .wait(function () {
            LoadApplicationViews();
        })
        .script("scripts/vendor/jquery.mobile-1.2.0.js")
        .wait(function () {
            // When ALL scripts have been loaded AND executed:              
        });
    </script>

    If you look at the code for LoadApplicationViews you will notice that it using the jQuery View Loader library to identify and load all templates (scripts of type text/html).
    Note: make sure you at least have version 0.3 of jQuery View Loader (to ensure compatibility with jQuery Mobile).

    function LoadApplicationViews() {
        if (typeof(console) != 'undefined' && console) 
            console.info("LoadApplicationViews starting ...");          
        // Configure the ViewLoader
        $(document).viewloader({
            logLevel: "debug",
            success: function (successfulResolution) {
                InitializeViewMediators();
            },          
            error: function (failedResolution) {
                // Loading failed somehow
                if (typeof(console) != 'undefined' && console) {
                    console.error("index.html page failed to load");    
                }
            }
        });     
        if (typeof(console) != 'undefined' && console) 
            console.info("LoadApplicationViews done ...");  
    }

    You should now be able to run the application and see the views appear (in the simulator or browser).
    Savings Goal Simulator Before jQuery Mobile Conversion
    As you can see, even though the app actually works, it is not really usable and we will definitely need adapt the layout and styling of the web app to fit the much smaller screen dimensions of a mobile device.

Adapting The UI For Mobile Use

Overall our goals will be to:

  • optimize the layout of all panels to ensure you can see all critical elements of the app
  • make input fields, buttons and controls larger so our fingers can easily work with them
  • adding sliders where appropriate to allow easy change to numeric values
  • using collapsible panels to maximize the use of space

So let’s start with the “savings goal” panel first.

  1. In Aptana Studio, expand the assets/www/script/views folder containing all our views, then expand index which contains partial views (with an underscore prefix ) for the index.html page, and open the _savings-goal.view.html page:
    Views Folder

    I would recommend to make a copy of the file and name it _savings-goal. web .view.html and change the reference to the file in index.web.html :

    <script id='savings-goal-view-template'
            type='text/html'
            src='scripts/view/index/_savings-goal.web.view.html' ></script>

    This way we can keep a baseline of what the web version looks like while we customize the mobile version.

  2. Currently, it consists simply of a section with pairs of label + input (or span) tags:

    <section id='savings-goal-view' >
        <label for="savings-goal-amount">Savings Goal Amount:</label>
        <input id="savings-goal-amount"  /><br/>        
        <label for="savings-max-duration">Savings Max Duration (In Months):</label>
        <input id="savings-max-duration" maxlength="3" /><br/>      
        <label for="savings-target-per-month">Savings Target Per Month:</label>
        <span id="savings-target-per-month"  /><br/>
    </section>
  3. Let’s wrap the first pair of label + input with a div with a data-role=”fieldcontain” so that jQuery Mobile can enhance it to make the field bigger.

    <div id="goal-amount" data-role="fieldcontain">
        <label for="savings-goal-amount">Savings Goal Amount:</label>
        <input id="savings-goal-amount"  /><br/>
    </div>
  4. Let’s repeat the process for the remaining pairs:

    <div id="max-duration" data-role="fieldcontain">
        <label for="savings-max-duration">Savings Max Duration (In Months):</label>
        <input id="savings-max-duration" maxlength="3" />
    </div>
    <div id="target-per-month" data-role="fieldcontain">
        <label for="savings-target-per-month">Savings Target Per Month:</label>
        <span id="savings-target-per-month"></span>
    </div>
  5. Let’s add a header above all data-role=”field-contain” blocks:

    <section id='savings-goal-view' >
        <header>
            <h2>Goal</h2>
        </header>
        <div id="goal-amount" data-role="fieldcontain">
            <label for="savings-goal-amount">Savings Goal Amount:</label>
            <input id="savings-goal-amount"  />
        </div>
        <div id="max-duration" data-role="fieldcontain">
            <label for="savings-max-duration">Savings Max Duration (In Months):</label>
            <input id="savings-max-duration" maxlength="3" />
        </div>
        <div id="target-per-month" data-role="fieldcontain">
            <label for="savings-target-per-month">Savings Target Per Month:</label>
            <span id="savings-target-per-month"></span>
        </div>
    </section>
  6. If you run the app in the simulator you will notice that sometimes the jQuery Mobile “look” appears and sometimes it does not. This is due to timing issues between the web view and jQuery Mobile. To remedy this, we’ll request a jQuery Mobile “refresh” of our page by triggering a create event on our page div. Let’s add that as the final script statement to execute once LAB.js has completed the processing of jquery.mobile-1.2.0.js:

    <script type="text/javascript">
        $LAB
        // ... shortened for brevity
        // ...
        .script("scripts/vendor/jquery.mobile-1.2.0.js")
        .wait(function () {
            // When ALL scripts have been loaded AND executed:
            setTimeout(function() {
                // Refresh the jQuery Mobile rendering
                $('#page-home').trigger('create');
            }, 50);
        });
    </script>
  7. Let’s run the app in the simulator again:
    jQueryMobile-ized Goal Panel Increment 1

  8. Now the 2 input fields are big enough for a phone but notice the type of keyboard that comes up if you bring focus to the amount field:
    Goal Amount With Text Keyboard
    This is a text keyboard. What we need is to tell the device to use a numeric keyboard. This is accomplished using new HTML5 attributes : type , min , and max . So let’s add them to the amount input tag:

    <input id="savings-goal-amount" type="number" min="1" max="1000" maxlength="4" />

    And let’s test again:
    Goal Amount With Numeric Keyboard
    Yippee, we now have a numeric keyboard, and notice that there is a Next button to navigate to the next input field!

  9. Let’s repeat the technique for the max number of months field:

    <input id="savings-max-duration"  type="number" min="1" max="120" maxlength="3" />
  10. Now we can use the appropriate numeric keypad to enter values, but it is still not very convenient if we want to vary the values and really test their impact. What we need is the jQuery Mobile slider.

  11. In AptanaStudio expand the assets/www/scripts/viewmediator folder and let’s open the sgs.mediator.savings-goal.js module which includes the view mediator responsible for the UI interactions between the view and the viewmodel. Look for the function named sgs.mediator.savingsgoal. setupViewDataBindings and let’s add the following statements to initialize sliders for our input fields:

    // Add sliders to our input fields
    $('#savings-goal-amount').slider(); 
    $("#savings-max-duration").slider();

    The whole function should now look like this:

    sgs.mediator.savingsgoal.setupViewDataBindings = function(renderedNodes, boundViewModel) {
        // Declare the HTML element-level data bindings
        $("#savings-goal-amount")       .attr("data-bind","value: savingsGoalAmount");
        $("#savings-max-duration")      .attr("data-bind","value: savingsMaxDuration");
        $("#savings-target-per-month")  .attr("data-bind","text: savingsTargetPerMonthFormatted()");        
        // Ask KnockoutJS to data-bind the view model to the view
        var viewNode = $('#savings-goal-view')[0];
        var viewModel = sgs.mediator.savingsgoal.getViewModel();                
        ko.applyBindings(viewModel, viewNode);              
        // Initialize default for value models linked to masked fields 
        var pageSettings = GetPageSettings();
        viewModel.savingsGoalAmount(pageSettings.defaultSavingsGoal || 0);
        viewModel.savingsMaxDuration(pageSettings.defaultSavingsMaxDuration || 0);      
        // Add sliders to our input fields      
        $('#savings-goal-amount').slider(); 
        $("#savings-max-duration").slider();            
        if (typeof(console) != 'undefined' && console) 
                console.info("sgs.mediator.savingsgoal.setupViewDataBindings done!");
    }

    Now let’s re-run the app, you should see the 2 sliders
    Savings Goal Panel With Sliders
    And moving the sliders should automatically change the values.

  12. Let’s shorten our labels to save space:

    • Shorten “Savings Goal Amount:” to just “Amount:”
    • Shorten “Savings Max Duration (In Months)” to just “Period (In Months):”
    • Shorten “Savings Target Per Month:” to “Per Month:”

Adapting The Stylesheets For Mobile Use

So far we have only used the default style sheets from jQuery Mobile. Let’s integrate the original application.css from the web version.

  1. In index.html , after the jQuery Mobile stylesheet, let’s include application.css :

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />                   
    <link href="css/jquery.mobile/jquery.mobile-1.2.0.css" rel="stylesheet" type="text/css"  />  
    <link href="css/jquery.mobile/jquery.mobile.structure-1.2.0.css" rel="stylesheet" type="text/css"  />   
    <link href="css/application.css" rel="stylesheet" type="text/css"  />
  2. To allow the application to adapt to different form factors, we will create different CSS files and direct the browser to use the appropriate stylesheet based on the media directive.

  3. Under assets/www/css , let’s create two new CSS files:

    • application. phone .css
    • application. tablet .css
  4. In index.html, after the include for application.css, let’s add two new include statements, each specifying what values the media query should match.

    <link href="css/application.tablet.css" type="text/css" rel="stylesheet" 
            media="only screen and (max-device-width: 1024px)" />

    This stylesheet should be used if the maximum device width is 1024px which usually means a tablet.

    <link href="css/application.iphone.css" type="text/css" rel="stylesheet" 
            media="only screen and (max-device-width: 480px)" />

    This stylesheet should be used if the maximum device width is 480px which usually means a phone.

  5. Now let’s tweak our original application.css

    • Let’s remove the styles details for the section element:

      section
      {
      }
    • Let’s remove the standard padding for the h2 element:

      section header h2
      {
          margin: 0;
      }
    • Let’s remove the styles details for the #savings-goal-view element:

      #savings-goal-view
      {   
      }
    • Let’s adjust the styles details for the #savings-goal-view label rule:

      #savings-goal-view label
      {
          display: inline-block;
          text-align: right;
          width: 90px;
      }
    • Let’s adjust the styles details for the #consumption-scenarios-view rule:

      #consumption-scenarios-view
      {
          margin: 5px 0;
      }
    • Let’s adjust the styles details for the #savings-forecast-view rule:

      #savings-forecast-view
      {
          margin: 5px 0;
      }
    • Let’s adjust a new rule to override the border and padding used in our jQuery Mobile ui-field-contain divs:

      .ui-field-contain
      {
          border: none;
          padding: 0;
      }
    • Let’s adjust a new rule to override the font used in our jQuery Mobile labels:

      .ui-field-contain label
      {
          font-size: 10px;
          margin: 2px 2px;
      }
    • Let’s adjust a new rule to override the width used in our jQuery Mobile sliders:

      div.ui-slider
      {
          width: 30%;
      }
    • Let’s create an override rule for the collapsible panels:

      .ui-collapsible-inset .ui-collapsible-content
      {
          border-color: black;
          border-style: solid;
          border-width: 1px;
      }
  6. Let’s run the app in the browser and simulator to see our new styling:
    Savings Goal Panel With New Styling

At this point we could continue adapting the look of the Savings Goal panel, by adjusting the CSS styles to make it fit a little bit more tightly. The easiest way to do that is to use your trusted browser developer tool to learn what styles are being used by jQuery Mobile and to experiment.

I will leave this as an exercise for the reader.

Adapting The Consumption Scenarios View For Mobile Use

In the web version of the application, the different type of coffee consumption options consists or set of radio buttons layed out in a table. But at minimum for a phone version of the app we need to conserve the vertical space so we can see the goals, a subset of the consumption options, and the savings forecast.

So let’s use collapsible panels for each type of consumption option such as “type” (of beverage), size, and frequency. This way as a user you can expand the option you want to play with, tweak the current and proposed settings, see the impact on the forecast, and repeat the process.

  1. First, let’s make a copy of the _consumption-scenarios.view.html file under assets/www/scripts/view/index and rename the copy to _consumption-scenarios. web .view.html and change the reference to the file in index.web.html :

    <script id='consumption-scenarios-view-template'
            type='text/html'
            src='scripts/view/index/_consumption-scenarios.web.view.html' ></script>

    Again we now have a baseline of the original web version.

  2. Now open the _consumption-scenarios.view.html file in Aptana Studio. Locate the header tag, and let’s wrap the “Weekly Coffee Consumption” title in an h2 tag:

    <header>
        <h2>Weekly Coffee Consumption</h2>
    </header>
  3. Right below the header but before the table, let’s add a div of [jQuery Mobile] data-role “ collapsible-set ” indicating that contained elements of data-role “collapsible” can be grouped together.

    <div id="weekly-consumption-accordion" data-role="collapsible-set">
    </div>
  4. Let’s add a div of data-role “ collapsible ” for our consumption type option. The div will contain a h3 for the panel title, followed by a table element to hold the details of our option

    <div id="weekly-consumption-accordion" data-role="collapsible-set">
        <div data-role="collapsible" data-theme="a"  data-mini="true" >
            <h3>Consumption Type</h3>
            <table>
                <tr>
                    <th>Current Habits</th>
                    <th>Proposed Change</th>
                </tr>
                <tr>
                </tr>
            </table>
        </div>
    </div>
  5. Now we can move the markup related to the drink types from the original table over to the second row of our new table within the collapsible div:

    <div id="weekly-consumption-accordion" data-role="collapsible-set">
        <div data-role="collapsible" data-theme="a"  data-mini="true" >
            <h3>Consumption Type</h3>
            <table>
                <tr>
                    <th>Current Habits</th>
                    <th>Proposed Change</th>
                </tr>
                <tr>
                    <td>
                        <div id="current-drink-type">
                            <input type="radio" id="current-drink-regular"  name="CurrentDrinkType" value="Regular" />
                                <label for="current-drink-regular">Regular</label><br/>
                            <input type="radio" id="current-drink-latte"  name="CurrentDrinkType" value="Latte" />
                                <label for="current-drink-latte">Latte</label><br/>
                            <input type="radio" id="current-drink-espresso"  name="CurrentDrinkType" value="Espresso" />
                                <label for="current-drink-espresso">Espresso</label><br/>
                        </div>
                    </td>
                    <td>
                        <div id="proposed-drink-type">
                            <input type="radio" id="proposed-drink-regular"  name="ProposedDrinkType" value="Regular" />
                                <label for="proposed-drink-regular">Regular</label><br/>
                            <input type="radio" id="proposed-drink-latte"  name="ProposedDrinkType" value="Latte" />
                                <label for="proposed-drink-latte">Latte</label><br/>
                            <input type="radio" id="proposed-drink-espresso"  name="ProposedDrinkType" value="Espresso" />
                                <label for="proposed-drink-espresso">Espresso</label><br/>
                        </div>
                    </td>
                </tr>
            </table>
        </div>
    </div>
  6. Let’s remove all the <br/> tags since we no longer need them since jQuery Mobile is enhancing our DOM and styling new elements.

  7. Let’s run in the browser and the simulator.
    Collapsible Consumption Type Panel

    You will notice a couple things:

    • The collapsible element does expand and collapse when we toggle it

    • jQuery Mobile has rendered our radio buttons as regular buttons. Apparently jQuery Mobile require a radio button group to be codified as a fieldset with a data-role of “ controlgroup “.
      So let’s replace our div for the current-drink-type:

      <div id="current-drink-type">
          <!-- ... -->
      </div>

      by a fieldset – note that we’ll request a “mini” version (using data-mini=”true”) to minimize the vertical space taken by the resulting jQuery Mobile radio button group:

      <fieldset id="current-drink-type" data-role="controlgroup" data-mini="true">
          <!-- ... -->
      </fieldset>

      Let’s re-run:
      Collapsible Consumption Type Panel

  8. Now let’s repeat the process for the other 2 types of consumption options (Size and Frequency):

    • Add a div of data-role “ collapsible ” for our consumption option. The div will contain a h3 for the panel title, followed by a table element to hold the details of our option.
    • Move the markup related to the consumption option from the original table over to the second row of our new table within the collapsible div
    • Change the div containing the radio buttons into a fieldset of data-role “radiobuttongroup”
  9. For the frequency collapsible panel, the “Other:” option has an input field associated with it. So we will need to wrap the input field with a div of data-role “field-contain”, and add HTML5 type, min and max attributes:

    <div data-role="fieldcontain">
        <input type="text" 
                id="current-custom-frequency" 
                name="CurrentCustomFrequency"
                type="number" min="1" max="999" maxlength="3" />
    </div>

    To allow the input field to be positioned to the right of the radio button, we will need to move the markup for data-role “fieldcontain” inside the label tag of the radio button like so:

    <label for="current-frequency-other">Other:
        <div data-role="fieldcontain">
            <input type="text" 
                    id="current-custom-frequency" 
                    name="CurrentCustomFrequency"
                    type="number" min="1" max="999" maxlength="3" />
        </div>
    </label>

    If you run in the browser or simulator, you will notice that the input field is not positioned where we would like it. So let’s add a CSS class on the div data-role “fieldcontain” named “ other-input “:

    <div data-role="fieldcontain" class="other-input">
            <input type="text" 
                    id="current-custom-frequency" 
                    name="CurrentCustomFrequency"
                    type="number" min="1" max="999" maxlength="3" />
        </div>

    And let’s define the new class in application.css :

    .other-input
    {
        display: inline-block;
        left: -20px;
        margin: 0 0 5px 0;
        top: -10px;
        width: 30px;    
    }
    
    
    .other-input input
    {
        height: 24px;
    }

    Let’s re-run:
    Collapsible Consumption Frequency Panel

    Now the positioning is much better!

  10. For the number of drinks per day, let’s create a separate collapsible panel like so:

    <div data-role="collapsible" data-theme="a"  data-mini="true" >
            <h3>Drinks Per Day</h3>
            <table>
                <tr>
                    <th>Current Habits</th>
                    <th>Proposed Change</th>
                </tr>
                <tr>
                    <td>
                        <input type="text" id="current-drinks-per-day" name="CurrentDrinksPerDay" />
                    </td>
                    <td>
                        <input type="text" id="proposed-drinks-per-day" name="ProposedDrinksPerDay" />
                    </td>
                </tr>               
            </table>
        </div>

    Let’s re-run:
    Collapsible Drinks Per Day Panel

  11. Now we need to cleanup the bottom remaining table in _consumption-scenarios.view.html.
    Let’s give the table an id of consumption-summary-table :

    <table id="consumption-summary-table">
    </table>

    Let’s cleanup the obsolete table header and let’s reuse it for our weekly summary.

    <thead>
        <tr>
            <th>Weekly Summary</th>
            <th>Current Habits</th>
            <th>Proposed Change</th>
        </tr>
    </thead>

    And remove the obsolete column titles:

    <tbody>
        <tr>                    
            <td>Type:</td>
        </tr>       
        <tr>
            <td>Size:</td>
        </tr>           
        <tr>
            <td>Frequency:</td>
        </tr>
        <!-- ... -->
    </tbody>

    Let’s define styling for consumption-summary-table in application.css:

    #consumption-summary-table
    {
        border-radius: 8px;
        background-color: white;
        width: 100%;
    }
    
    
    #consumption-summary-table th
    {
        border-bottom: 1px solid Grey;  
    }

    Let’s re-run:
    Collapsible Consumption Option Panels

    You will notice that we’re pretty close to our goal of optimizing for the vertical height of a phone!

Adapting The Savings Forecast View For Mobile Use

  1. First, let’s make a copy of the _savings-forecast.view.html file under assets/www/scripts/view/index and rename the copy to _savings-forecast. web .view.html and change the reference to the file in index.web.html :

    <script id='savings-forecast-view-template'
            type='text/html'
            src='scripts/view/index/__savings-forecast.web.view.html' ></script>

    Again we now have a baseline of the original web version.

  2. Now open the _savings-forecast.view.html file in Aptana Studio. To maximize vertical space, let’s turn the current markup into a horizontal table where the labels become column headers. We’ll also shorten the column headers and the unit of measure in each data cell:

    <section id='savings-forecast-view'>
        <table id='savings-forecast-table'> 
            <tr>
                <th>Forecast</th>
                <th>Variance</th>
                <th>Months</th>
            </tr>
            <tr>
                <td><span id="savings-forecast-per-month" ></span>/m</td>
                <td><span id="forecast-variance-per-month"></span>/m</td>
                <td><span id="time-to-goal-in-months"></span>m</td>
            </tr>
        </table>
    </section>
  3. Let’s define styles for our new table in application.css:

    #savings-forecast-table {
        width: 100%;
    }
    
    
    #savings-forecast-table tr td {
        text-align: center;
    }
    
    
    #savings-forecast-view input
    {
        width: 30px;
    }

    Run the app:
    Forecast Panel

  4. Ideally we would like the forecast panel to be visible at all times. An idea is to integrate it in the footer of the page and “ pin ” the footer at the bottom of the screen.
    So open the index.html page, and let’s move the script include for the savings-forecast-view-template and the actual savings-forecast-view-container div into the footer:

    <div data-role="footer" data-position="fixed"  data-theme="e">
        <script id='savings-forecast-view-template'
                type='text/html'
                src='scripts/view/index/_savings-forecast.view.html' ></script>
        <div id='savings-forecast-view-container'
                data-bind='template: { name: "savings-forecast-view-template", afterRender: sgs.mediator.savingsforecast.setupViewDataBindings } ' ></div>
    </div>

    To “ pin ” the header, let’s add a data-position=”fixed” and data-fullscreen=”true” as attributes on the footer. And while were at it let’s specify a different jQuery Mobile theme for the footer using data-theme=”e” .

    <div data-role="footer" data-position="fixed" data-fullscreen="true" data-theme="e">
        <!-- -->
    </div>

    Let’s refresh:
    Forecast Panel

    This is much nicer!

More Visual Tweaks

  1. In the index.html , let’s customize the jQuery Mobile theme to be used on all its widgets

    <div id="page-home" data-role="page" data-content-theme="b">
        <!-- ... -->
    </div>
  2. Let’s increase the horizontal padding for all section elements:

    section
    {
        padding: 0px 2px;
    }
  3. Let’s tweak the height of the #page-home div and its data-role=”content” div:

    #page-home 
    {
        height: 100%;
        overflow: hidden;
    }
    
    
    #page-home div.ui-content
    {
        height: 88%;
    }
  4. Let’s move the “Per Month” elements from the bottom of the savings goal panel to the section header:

    <header>
        <h2>Goal</h2>
        <div id="target-per-month" data-role="fieldcontain">
            <label for="savings-target-per-month">Per Month:</label>
            <span id="savings-target-per-month"></span>
        </div>
    </header>

    And let’s add the following styles to application.css:

    #savings-goal-view header h2 {
        display: inline-block; 
    }
    
    
    #target-per-month {
      display: inline-block;
      right: -114px;
    }
    
    
    #savings-goal-view #target-per-month label
    {
        min-width: 30px;
    }
    
    
    #savings-goal-view #savings-target-per-month {
        font-size: 0.8em;
    }
  5. Let’s remove the word “Consumption” from our collapsible panels.

  6. Let’s re-run:
    New Look

    The look is now a bit more “professional”, the visual elements stand out and the vertical positioning is optimized.

UI “Feel” / Behavior Tweaks

Originally the web app depended on jQuery UI, but since we’re usin jQuery Mobile instead, we will need to tweak the mediators associated with our panels.

  1. Two view mediators use the jQuery UI highlight effect to provide a visual indication of key changes. But now that we have sliders, the effect would take too long since it would be triggered on each mouse move. So let’s remove the effect.

  2. Open sgs.mediator.savings-goal.js under assets\www\scripts\viewmediator and look for a call to the effect function:

    viewModel.savingsTargetPerMonthFormatted.subscribe(function() {
        $("#savings-target-per-month")
            .effect('highlight', { color: 'LightGreen' }, 3000); // for 3 seconds
    });

    and let’s remove the overall statement altogether:

    viewModel.savingsTargetPerMonthFormatted.subscribe(function() {
        // Placeholder for future use
    });
  3. Open sgs.mediator.consumption-scenarios.js and repeat the process:

    viewModel.savingsPerWeekFormatted.subscribe(function() {
        // Placeholder for future use
    });

So if you had been noticing exceptions for the effect call in the browser console, now these are a thing of the past.

Adding An Other jQuery Mobile Page

Currently our application has only one jQuery Mobile “ logical page “, that is, a div with a data-role=”page” . It would be nice if we could have another page to edit the coffee beverage list prices! jQuery Mobile allow the definition of multiple pages and provide associated navigation mechanisms. So let’s see how to do that.

  1. First, in the index.html , before the end of the body tag, let’s add the shell for our new page:

    <div id="coffee-pricing-dialog"
         data-role="page" 
         data-content-theme="b" 
         data-url="edit-coffee-pricing.html" >
            Shell for our price list!
           </div>
    </div>
  2. We need a button to navigate to the new page. Let’s edit _consumption-scenarios.view.html and add a button (div with data-role=”button” ) in the header , after the h2 title:

    <section id='consumption-scenarios-view'>
        <header>
            <h2>Weekly Coffee Consumption</h2>
            <a id="edit-pricing-button" href="#coffee-pricing-dialog" class="ui-btn-right"
                data-rel="dialog" 
                data-role="button" 
                data-mini="true"
                data-icon="gear"
                data-theme="e" >Pricing</a>
        </header>
        <!-- ... ---> 
    </section>

    You will notice that we specified a data-rel attribute with a value of “ dialog “. This tells jQuery Mobile that we would like a pop-up transition effect and a dialog theme: dark background, rounded corners, etc.. See the Dialog documentation for more details.

    If you run the app in the simulator, you should see the new Pricing button:
    New Pricing Button

    And clicking on it will cause the navigation to the new page to occur:
    New Pricing Panel Shell

    If you hit the back button, the page-home page will be displayed again (without any of our values to be lost – since the pages are just hidden/shown during the navigation process).

  3. Like for other panels, let’s externalize the markup for our new pricing page. So let’s create a new markup folder (one for each page) under assets/www/scripts/ views/named “ pricing “, and then create a file named index.view.html , an cut/paste our shell message inside the following template:

    <section id='coffee-pricing-view'>
        <header data-role="header">
            <h3>Edit Pricing</h3>
        </header>           
        <div data-role="content">
            Shell for our price list!
        </div>
    </section>
  4. To render the externalized view we have to:

    • include a text/html template script reference in the main index.html file
    • create a container div data-bound to the template
    • ask KnockoutJS to render it.

    So remove the “Shell for our price list!” content from the coffee-pricing-dialog div and replace it with:

    <script id='coffee-pricing-view-template'
            type='text/html'
            src='scripts/view/pricing/index.view.html' >
    </script>

    This allows the inclusion of our new jQuery template in the app.
    And right below let’s add our div container:

    <div id='coffee-pricing-view-container'
            data-bind='template: { name: "coffee-pricing-view-template", afterRender: sgs.mediator.coffeepricing.setupViewDataBindings} ' >
    </div>

    Note: the afterRender callback references a setupViewDataBindings function we have not yet created in the sgs.mediator.coffeepricing module. So if you run the app now, the content of the view will not be rendered.

  5. Open up the sgs.mediator.coffeepricing.js module under assets/www/scripts/viewmediator and let’s create a shell for the setupViewDataBindings function:

    sgs.mediator.coffeepricing.setupViewDataBindings = function(renderedNodes, boundViewModel) {
    
    
    }

    Then let’s update the createViewMediator function to request data binding of the coffee pricing view container with the template . This will cause KnockoutJS to render the template using jQuery Template:

    sgs.mediator.coffeepricing.createViewMediator = function (pageSettings) {
        // Create the view Pricing Editor view-specific view model
        var viewModel = sgs.model.coffeepricing.initializeViewModel(pageSettings);
        // Save the view model
        sgs.mediator.coffeepricing.setViewModel(viewModel);         
        // Ask KnockoutJS to data-bind the view model to the view
        var viewNode = $('#coffee-pricing-view-container')[0];
        var viewModel = sgs.mediator.coffeepricing.getViewModel();
        ko.applyBindings(viewModel, viewNode);      
        if (typeof(console) != 'undefined' && console) 
            console.info("sgs.mediator.coffeepricing ready!");  
    }

    Now we can expand the code for setupViewDataBindings to data-bind the coffee pricing view div with our KnockoutJS coffee pricing view model :

    sgs.mediator.coffeepricing.setupViewDataBindings = function(renderedNodes, boundViewModel) {
        // Apply the bindings
        var viewNode = $('#coffee-pricing-view')[0];
        var viewModel = sgs.mediator.coffeepricing.getViewModel();
        ko.applyBindings(viewModel, viewNode);
        // Get jQuery Mobile to refresh/enhance the page        
        $('#coffee-pricing-dialog').trigger('create');
    }

    Now if run the app and click on the pricing button we should see our new pricing shell page:
    Externalized Pricing Panel Shell

  6. Let’s add some input fields to our pricing/ index.view.html view to capture the pricing data:

    <section id='coffee-pricing-view'>
        <header data-role="header">
            <h3>Edit Pricing</h3>
        </header>           
        <div data-role="content">
            <div class="ui-bar ui-bar-b" data-position="inline">
                Regular Coffee
            </div>
            <div data-role="fieldcontain">
                <label for="pricing-regular-tall">Tall:</label>
                <input id="pricing-regular-tall" type="number" min="1" max="5" maxlength="4" />
            </div>              
            <div data-role="fieldcontain">
                <label for="pricing-regular-grande">Grande:</label>
                <input id="pricing-regular-grande" type="number" min="1" max="5" maxlength="4" />
            </div>              
            <div data-role="fieldcontain">
                <label for="pricing-regular-venti">Venti:</label>
                <input id="pricing-regular-venti" type="number" min="1" max="5" maxlength="4" />
            </div>      
        </div>
    </section>

    Regular Coffee Pricing Shell

    You can repeat the process for the beverages like caffe latte and espresso.

    If you run the app you will see an empty dialog since we have not created a value models for each of the pricing options in the pricing view model, nor added the data binding for the various fields yet.

  7. If we look at the current definition of our coffee pricing view model , we’ll see that it includes only one value model : “ pricing “:

    sgs.model.coffeepricing.initializeViewModel = function (pageSettings) {  
        // ...          
        // Lazy-initialize the price list
        var priceList = sgs.model.coffeepricing.getPriceList();         
        var viewModel = {
            pricing:        ko.observable(priceList)
        }  
        // ...          
        return viewModel;  
    }

    If we evaluate / inspect the both the view model and the pricing value model in the browser tools console we’ll see:

    Current Coffee Pricing ViewModel

    But so the coffee pricing view model does not have a separate value model for each property of the pricing value model. We have two options: a) declare a value model for each property, or b) leverage the dynamic nature of Javascript to dynamically declare a new value model for each property of our pricing object. Let’s choose the latter approach.
    Let’s add a new function named publishPriceList to our sgs.model.coffeepricing module.

    sgs.model.coffeepricing.publishPriceList = function (viewModel) {
        var priceList = viewModel.pricing();
        // Find all property names for the pricing object
        var productKeys = Object.keys(priceList);           
        // Process all properties
        for ( var i = 0; i < productKeys.length; i++) {
            // Get product details
            var productKey = productKeys[i];
            var productPrice = priceList[productKey];
            // Create an observable name based on the property
            var observableName = 'pricing' + productKey.replace('-','');        
            // Check if we already have value model (observable) with the same name
            var valueModel = viewModel[observableName];
            if (typeof(valueModel) == 'undefined') {
                // Dynamically create a value model
                valueModel = ko.observable(null);
                // Add it to the overall view model
                viewModel[observableName] = valueModel;
            }               
            // Update the value model's actual pricing
            valueModel(productPrice);
        }
    }

    You can test it in the browser console by evaluating:

    sgs.model.coffeepricing.publishPriceList(sgs.mediator.coffeepricing.getViewModel())

    Enhanced Coffee Pricing ViewModel

    You now see the new value models!
    So let’s add a call to the publishPriceList function from initializeViewModel :

    sgs.model.coffeepricing.initializeViewModel = function (pageSettings) {
        // ..
        var priceList = sgs.model.coffeepricing.getPriceList();         
        var viewModel = {
            pricing:        ko.observable(priceList)
        }           
        // Publish the pricing data as individual value models
        sgs.model.coffeepricing.publishPriceList(viewModel);            
        // ...
        return viewModel;
    }

    This way the new value models will be created upon initialization of the view model.

  8. Now we can add the corresponding data bindings in the setupViewDataBindings function of the sgs.mediator.coffee-pricing.js module:

    sgs.mediator.coffeepricing.setupViewDataBindings = function(renderedNodes, boundViewModel) {
        // Ask KnockoutJS to data-bind the view model to the view
        $("#pricing-regular-tall").attr("data-bind","value: pricingRegularTall");
        $("#pricing-regular-grande").attr("data-bind","value: pricingRegularGrande");
        $("#pricing-regular-venti").attr("data-bind","value: pricingRegularVenti");
        // Apply the bindings
        var viewNode = $('#coffee-pricing-view')[0];
        var viewModel = sgs.mediator.coffeepricing.getViewModel();
        ko.applyBindings(viewModel, viewNode);
        // Get jQuery Mobile to refresh/enhance the page
        $('#coffee-pricing-dialog').trigger('create');
    }

    If we re-run the app we now see the pricing details:
    Regular Coffee Pricing

    And if you update a price, close the dialog, then reopen it the new pricing is still there. But you might notice that the calculations are not reflecting the price update. And if you closed and terminated the mobile app and relaunched it your new price would be lost.

  9. What we need is to add logic to save the new pricing data once we’re done with our updates on the pricing dialog. First, let’s add a save button to the pricing/ index.view.html view:

    <section id='coffee-pricing-view'>
        <!-- ... -->            
        <div data-role="content">
        <!-- ... -->            
            <a id="save-pricing-button" href="#" 
                    data-rel="back"
                    data-role="button" 
                    data-icon="check"
                    data-theme="e" >Save Pricing</a>
        </div>
    </section>

    Save Pricing Button

  10. Let’s add a click event handler for our new button in the setupViewDataBindings function of the sgs.mediator.coffee-pricing.js module.

    sgs.mediator.coffeepricing.setupViewDataBindings = function(renderedNodes, boundViewModel) {
        // ...  
        // Get jQuery Mobile to refresh/enhance the page
        $('#coffee-pricing-dialog').trigger('create');          
        // Add a click handler of the Save Pricing button
        $('#save-pricing-button')
            .die('click')
            .bind('click',sgs.mediator.coffeepricing.savePriceList);            
    }
  11. Le’s define the sgs.mediator.coffeepricing.savePriceList and make it call savePriceList in sgs.model.coffee-pricing.js module:

    sgs.mediator.coffeepricing.savePriceList = function() {
        sgs.model.coffeepricing.savePriceList();
    }
  12. We just need to implement the new savePriceList function in the sgs.model.coffee-pricing.js module. For now let’s create a shell:

    sgs.model.coffeepricing.savePriceList = function () {  
    }
  13. The requirements for the savePriceList are to:

    • Apply the current data of the individual price value models back to the master pricing value model
    • Save our pricing value model data to local storage using the jStorage library

    Here is the code:

    sgs.model.coffeepricing.savePriceList = function () {
        var viewModel = sgs.mediator.coffeepricing.getViewModel();
        var priceList = viewModel.pricing();
        // Find all property names for the pricing object
        var productKeys = Object.keys(priceList);
        // Process all properties
        for ( var i = 0; i < productKeys.length; i++) {
            // Get product details
            var productKey = productKeys[i];
            // Find the corresponding observable
            var observableName = 'pricing' + productKey.replace('-','');
            var valueModel = viewModel[observableName];
            // Update the value model
            var productPrice = parseFloat(valueModel());
            priceList[productKey] = productPrice;
        }
        // Store the updated pricing in local storage
        $.jStorage.set("coffee-price-list", priceList);
    }
  14. If you re-run the app, you now should be able to see the impact of updating the pricing of a regular tall coffee from 1.4 to 1.5. And if you restart the app you should see that your pricing data had been saved and restored.
    In the browser developer toolls, you should be able to inspect the local storage and confirm that your can see your pricing data:

    Pricing In Local Storage

At this point you have a well-rounded mobile version of the Savings Goal Simulator!

Note: as an exercise for the reader, go ahead and externalize the content of the #page-home as a separate template named index.view.html in the assets/www/views/index folder. This will keep the overall index.html main page as small as possible.

Phone And Table Versions Of The App

Currently we have an index.html which is primarily designed to fit on a phone. And although it will take the full size of the display on a tablet, the application will still work fine. But what if we want to offer a more optimized design for a tablet such as for example do away with collapsible sections, or change the layout of the app?

Well, this is where the modularity of our views and application, plus the use of HTML and CSS will pay off.

Two approaches can be considered based on your design need:

  1. Have a separate main web page for the phone and for the tablet version and launch the appropriate page based on the device display size.
  2. Keep a single web page, but just custom CSS stylesheets to tweak the layout as needed

The first option provide the most flexibility, so let’s look at how to implement it.

Let’s look back at the code we used in the “controller” or “launcher” (implemented as Java Activity on the Android platform):

package com.yournamespace.savingsgoalsimulator;

    import android.os.Bundle;
    import org.apache.cordova.*;

    public class MainActivity extends DroidGap {

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            super.loadUrl("file:///android_asset/www/index.html");
        }

    }

At a high-level, we need to:

  • Rename our index.html to index. phone .html
  • Clone our index.phone.html into a new index. tablet .html
  • Change the onCreate method to detect the device size and load index.phone.html or index.tablet.html

Here is an implementation for onCreate based on a check for the width of the device:

@Override
        public void onCreate(Bundle savedInstanceState) {
            protected float TABLET_WIDTH = 768;

            super.onCreate(savedInstanceState);

            // Get actual screen size so we can determine if the device is a tablet or not
            Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
            int width  = display.getWidth(); 
            int height = display.getHeight(); 

            // Is the width is at least the minimum width of a tablet?
            boolean isTablet = (width >= TABLET_WIDTH); 
            Log.v("Device size detection", "width=" + width + " height=" + height + " isTablet=" + isTablet );

            // Determine the name of the corresponding main page
            String deviceType = isTablet ? "tablet" : "phone";
            String pageUrl = "file:///android_asset/www/index." + deviceType + ".html";

            super.loadUrl(pageUrl);
        }

Now you can customize the index.tablet.html page to your liking, such as for example:

  • Adding new functionality
  • Adding new jQuery Mobile logical pages
  • Adding new template views
  • Splitting existing views into separate phone and tablet versions

Note: we had already included a media query and conditional inclusion of device-specific CSS (see the “Adapting The Stylesheets For Mobile Use” section details) in our main html page. So now you can expand the customization to suit the appropriate device.

Prepare For Packaging

Since packaging is different for each device OS platform, I would recommend consulting the corresponding platform documentation. But the following items for consideration apply to all platforms:

  • Creating the application icon in several resolutions
  • Creating the initial screen image which is displayed while the app is loading or being re-activated
  • Applying code signing certificates

Hosting The Application Package On An “AppStore”

Of course, if your mobile application is for public consumption (for free or for sale) then the hosting guidelines of your platform apply (e.g. Apple AppStore, Google Play, Microsoft Windows Phone App Store).

But if your mobile application is for enterprise consumption, provided that you are enrolled in an enterprise program (e.g. iOS Developer Enterprise Program), you can create your own “custom app store” as a web application, require user authentication and authorization, tailor the app catalog based on user privileges. In that situation, each app has a link to either the package (for Android) or an application descriptor (for iOS – .plist file) which contains the location of your application package (e.g. Android .apk file, iOS .ipa file). Clicking on the link triggers an “Over The Air” installation of the application once you have approved the process.

This section was meant to just give you a sense of the possibilities, but you will need to refer the corresponding platform documentation for details.

5. Recap

Even though we touched on all the basic minimum aspects of a PhoneGap + jQuery Mobile app, in some ways we barely scratched the surface. For example, we did not cover the following topics:

  • leveraging device capabilities such as media, geolocation, etc. – but you easily lookup the corresponding details in the PhoneGap documentation
  • using PhoneGap plugins such as the mobile notifications
  • dynamically altering jQuery Mobile-enhanced HTML controls – in a nutshell you would create custom bindingHandlers for KnockoutJS
  • interfacing with web services (via AJAX)

These more advanced topics will be the subject of a separate tutorial.

Here is a quick recap of the steps we followed:

#
To Do
Area
1
  • Create a PhoneGap project for your mobile platform
  • Create a www folder structure under /assets for all types of resources:
    • www/css
    • www/css/jquery.mobile
    • www/scripts
    • www/scripts/vendor
    • www/scripts/view
    • www/scripts/viewmediator
    • www/scripts/viewmodel
  • Add external JS libraries including Cordova, jQuery, jQuery Mobile, etc. under www/scripts/vendor
  • Add the jQuery Mobile stylesheets and theme content under www/css/jquery.mobile
  • Create a shell page for each type of device:
    • index.phone.html
    • index.tablet.html
  • Customize the application controller (Activity in Android, AppDelegate in iOS) logic to detect the device display dimensions and launch the phone or tablet version of the index page
  • Under www/scripts, create an application.js module with the following function placeholders:
    • InitializeApplication
    • InitializeViewMediators
    • LoadApplicationViews
    • OnLoad
    • OnDeviceReady
  • Wire-up the onload event on the body tag of both index pages to the OnLoad function
Application
2
  • Customize the application style sheets under www/css
    • application.css
    • application.phone.css
    • application.tablet.css
  • Starting with the index.phone.html:
    • include core JS libraries,
    • add logic to asynchronously load other libraries via LAB.js
    • add a call to LoadApplicationViews (from application.js) once LAB.js has completed all loading
    • include stylesheets
  • Customize the LoadApplicationViews (from application.js) to use the jQuery View Loader to load all views and call InitializeViewMediators once done
View
3
For each “logical page” in the app:
  • Create a folder under www/scripts/view to group all markup templates for that page
  • Create an index.view.html for the page in the new folder
  • Add a div with data-role=’page’ in the body of both index.phone.html and index.tablet.html
  • Inside the new div, add a script tag referencing the index.view.html template from the page folder
  • Inside the new div, add a container div with data-bind=’template: { … }’ binding the div with the template
  • Create templates for any sub-panels of the page with the naming convention: _{panel-name}.view.html
View
4
For each view model you need (e.g one global plus one per logical page):
  • Create a Javascript module under www/scripts/viewmodel
  • Customize the namespace
  • Define a function named initializeViewModel to instantiate and return a view model and define value models (observables) and dependent observable functions
View Model
4
For each view mediator you need (e.g one per logical page):
  • Create a Javascript module under www/scripts/viewmediator
  • Customize the namespace
  • Define a function named createViewMediator to:
    • request a corresponding view model
    • save off the view model
    • bind the corresponding container div with its template
  • Define a function named setupViewDataBindings to bind each element of the page/view with their corresponding value model
  • If you need custom pre page rendering logic, create a function and wire it up to the pageinit event in the LAB.js post-loading code section
View Mediator
4
Customize the application initialization:
  • Update InitializeApplication with any custom initialization logic based on device data or features
Application
4
Update the InitializeViewMediators function of the www/scripts/application.js for each “logical page”:
  • Add a call to the createViewMediator of each mediator
Application

The “ PhoneGap + jQuery Mobile + jQuery ViewLoader + Knockout JSstack allows you to create portable mobile apps by leveraging your existing HTML5 and Javascript skills, while minimizing your need to master all the details of each platform.

This is a big deal because you can:

  • achieve a much greater portable code base (the platform-specific code base will be small)
  • get started quickly
  • use your favorite IDE
  • style your mobile app using CSS3 (including transitions and effects)
  • adopt the look of the base platform “automagically” (via jQuery Mobile rendering)
  • debug a great portion of the app using standard browser tools (e.g.: Google Chrome, Firefox + Firebug, …)
  • target one platform initially then expand to other ones over time

So if you have solid web skills it is time for you to apply them towards the creation of mobile apps!

References And Resources

PhoneGap Platform
Android Platform
Javascript Frameworks
Miscellaneous
Other Tutorials In The Same Series
Savings Goal Simulator
If you enjoyed this post, I would love it if you could check out mySkillsMap , my skills management app.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值