mobile

Using Node.js and Charles Proxy to Mock a Server API

While working on an iPhone project for a client, I came across a situation where I needed to test some error conditions returned from a server API which I have no control over.  I ended up using Charles Proxy to remote map the server API calls to a locally running Node.js server.  Of course the proper way to do this is to write functional tests and mock the networking classes to return the appropriate responses, but that is a blog post for another day.

In this post, I’ll walk through the steps to create a simple iPhone app that interacts with a public API, using Node and Charles to simulate an error response.  This post assumes some experience writing iOS code, but all the source code will be available at https://github.com/stevenpsmith/charles-node-blog.

Create an iOS App

For the iOS app, I’ll write a simple weather app that uses the OpenWeatherMap API.  We all know the world needs yet another weather app :).

Let’s start by creating a new single view project in Xcode.  In my iOS projects I almost always use CocoaPods for dependency management.  CocoaPods makes it easy to add/remove 3rd party libraries to your iOS project.  In this app, we’ll use AFNetworking to access the API, and I’ll add MBProgressHUD in case the response time is slow and the user needs to wait for UI updates. The following assumes that you already have CocoaPods installed.

After creating the Xcode project, quit Xcode (or close the project) and create a text file named “Podfile” at the root of the project.  The contents should include the following:

platform :ios, '7.0'

pod 'AFNetworking', '~> 2.0.3'
pod 'MBProgressHUD', '~> 0.8'

With that file in place, drop to a command line and execute pod install

That will create a workspace and configure the libraries. From this point forward, we use the workspace instead of the project. The final version of the application will look like this:

Screenshot_1_14_14__12_03_PM

The code base can be found at https://github.com/stevenpsmith/charles-node-blog, but the key portion is the API call, which is below (without all the details)

[[CSMServerAPI sharedInstance] weatherForLongitude:-75.1914 
                                       andLatitude:40.1386 
    successBlock:^(NSDictionary *weatherDict) {
        // code to map API response to UI elements here
        //...
    } failureBlock:^(NSError *error) {
        //access error message parts from response here
        //....
        NSString *msg = ...
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Houston - We have a Problem" 
                                                        message:msg 
                                                       delegate:nil 
                                              cancelButtonTitle:@"OK" 
                                              otherButtonTitles:nil];
        [alert show];
    }];

If everything is successful, the UI gets populated with the weather data. But, if there is a failure, we throw an alert up to the user containing the error messages (I know, alerts are not user friendly, but this is an example).

Mocking an error with Node

Now that we have the API calls in place, we need to set up our node server to fake an error response. Sticking with the theme of keeping things simple, I used the express framework for node. With node installed, I created a directory for my server app and added a ‘package.json’ file inside. The ‘package.json’ looks like the following:

{
  "name": "mock-api",
  "description": "for use with charles proxy to mock error conditions for OpenWeatherMap API",
  "version": "0.0.1",
  "dependencies": {
    "express": "~3.4.7"
  }
}

Dropping to a command line and executing npm install will set up the node server with the express module. Now we create an ‘app.js’ file and add our mock API, as follows

var express = require('express');
var app = express();

var errorJson = {error:"API Error",error_msg:"You have encountered an error thanks to Charles and Node....have a nice day!"};

app.get('/weather', function(req, res) {
  	res.json(400, errorJson);
});

app.listen(process.env.PORT || 4730);

Since our call to the OpenWeatherMap API is a get request to the /weather URI, we simply handle that request route in our code and return the response we want, which is an error containing a JSON error message. To start the server, execute node app.js at the command line and the server will be listening on port 4730.

Configure Charles Proxy for Remote Mapping

If you code against server APIs and don’t use Charles Proxy, or something like it, you really should start. It is immensely valuable for troubleshooting API calls, and the more I use it, the more nice features I find. I’ve used it for bandwidth throttling, request/response inspection, breakpoints (to change requests or responses), and now URL mapping. In order to map the OpenWeatherMap API to our local node server, access the Map Remote settings via the Tools -> Map Remote... menu. Once there, add a new remote mapping, filling out the fields as shown below. The configuration was also exported and is available at https://github.com/stevenpsmith/charles-node-blog.

Edit_Mapping_and_Map_Remote_Settings_and_Charles_3_8_3_-_Session_1

With that in place, enable remote mapping, run the iPhone app and get the following error:

Screenshot_1_14_14__11_51_AM

If you used Charles to record your network requests, you can even see the mapping occurred in the request overview notes. That’s all there is too it. You can certainly do a lot more than just simulate errors with this set up. I’ve used it to simulate server application errors as well, where server returns an http 200, but the response includes some application specific errors. Timeouts can be tested as well using the connect-timeout module. There is an example in the source code for that too.

So this is one way to intercept your API calls and modify them for testing. This is certainly no replacement for good functional tests, but simply another thing to add to your developer toolbox.

Securing Data in iOS Apps

There are numerous ways to secure data that you are storing on an iOS device.

The Simple/Built-in Way

The simplest way is to take advantage of iOS Data Protection (iOS 4+). This is accomplished by setting an attribute on a file like this:

    [[NSFileManager defaultManager] createFileAtPath:[self filePath]
                    contents:[@"super secret file contents" dataUsingEncoding:NSUTF8StringEncoding]
                    attributes:[NSDictionary dictionaryWithObject:NSFileProtectionComplete
                                                           forKey:NSFileProtectionKey]];

There are several different levels of file protection. The following is taken from the NSFileManager class reference from Apple:

  • NSFileProtectionNone – no file protection
  • NSFileProtectionComplete – file is encrypted when the device is locked or booting
  • NSFileProtectionCompleteUnlessOpen – file is encrypted and can only be opened when the device is unlocked. Once open, the file can continue to accessed even if the user locks the device.
  • NSFileProtectionCompleteUntilFirstUserAuthentication – The file is stored in an encrypted format on disk and cannot be accessed until after the device has booted. After the user unlocks the device for the first time, your application can access the file and continue to access it even if the user subsequently locks the device.

A Core Data sqlite store can also be encrypted by setting the NSFileProtectionKey file attribute to one of the above values (after you create your persistent store coordinator).

However, an important thing to realize is that this type of data protection requires the device to have a passcode set on it (http://support.apple.com/kb/HT4175).

CommonCrypto

What if you really need to insure that your data is protected regardless of whether the device has a passcode set? One way is to use the CommonCrypto libraries from Apple. This can be fairly complex. There is a great write up here from Rob Napier on what’s involved. Fortunately, he has also provided a wrapper that greatly simplifies this process here. Using this library (or writing your own), you can encrypt your data and store it wherever you need. If you are using Core Data, you could write an NSValueTransformer for the Core Data entity attributes that require encryption using CommonCrypto or the RNCrypto library to encrypt/decrypt the attribute values when accessing or updating them.

SQLCipher

One more way to protect your data, specifically data that you want to store in a SQLite database, would be to use SQLCipher. SQLCipher encrypts/decrypts data at the page level and is transparent to your application code. You still use the standard SQLite APIs, with one additional method call when accessing the database (passing your key to sqlite). There are excellent instructions on setting it up for use in an iOS project here.

You can build SQLCipher as a set of static libraries by following the same iOS instructions with some modifications and additions. Here are the high level steps:

  1. Instead of creating a view based iOS project, create a static library project
  2. Follow the balance of the steps for including SQLCipher in your project
  3. Add a new “Aggregate” build target
  4. Add a “Run Script” build phase and paste the following script into it. This will build both the simulator and iOS based targets for the libcrypto, libsqlcipher, and libssl libraries.
  5. xcodebuild -project ${PROJECT_NAME}.xcodeproj -sdk iphonesimulator -target ${PROJECT_NAME} -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator
    
    xcodebuild -project ${PROJECT_NAME}.xcodeproj -sdk iphoneos -target ${PROJECT_NAME} -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos
    
  6. Add another “Run Script” build phase and paste the following script into it. This will merge the simulator and iOS builds into one for each library. These three libraries are what would be added to a project using SQLCipher.
  7. CRYPTO_LIB="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/libcrypto.a" &&
    SQLCIPHER_LIB="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/libsqlcipher.a" &&
    SSL_LIB="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/libssl.a" &&
    
    DEVICE_CRYPTO_LIB="${BUILD_DIR}/${CONFIGURATION}-iphoneos/libcrypto.a" &&
    DEVICE_SQLCIPHER_LIB="${BUILD_DIR}/${CONFIGURATION}-iphoneos/libsqlcipher.a" &&
    DEVICE_SSL_LIB="${BUILD_DIR}/${CONFIGURATION}-iphoneos/libssl.a" &&
    
    UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal" &&
    
    
    # Create framework directory structure.
    rm -rf "${UNIVERSAL_LIBRARY_DIR}" &&
    mkdir -p "${UNIVERSAL_LIBRARY_DIR}" &&
    
    # Generate universal binary for the device and simulator.
    lipo "${CRYPTO_LIB}" "${DEVICE_CRYPTO_LIB}" -create -output "${UNIVERSAL_LIBRARY_DIR}/libcrypto" &&
    lipo "${SQLCIPHER_LIB}" "${DEVICE_SQLCIPHER_LIB}" -create -output "${UNIVERSAL_LIBRARY_DIR}/libsqlcipher" &&
    lipo "${SSL_LIB}" "${DEVICE_SSL_LIB}" -create -output "${UNIVERSAL_LIBRARY_DIR}/libssl"
    

Once completed, you can build your new aggregate target and there should be 3 libraries located in a subdirectory within your project build directory. Just add these to your XCode project as you would any other library and you are good to go.

I have also posted this article on my companies blog, located here.

Using JQuery Mobile, Backbone.js for Handling Forms

Introduction

In this post, I continue the development of my basic “exercise app” that I started (and enhanced) in these posts:

Let’s add the ability to create and edit exercise records.

Adding a new activity

Just like in the earlier post, the first thing we need is a jQueryMobile page to hold the form content. Add the following to index.html:

    <div data-role="page" id="activity-form" data-add-back-btn="true">
      <div data-role="header">
        <a href="#" data-role="button" data-icon="check" data-theme="b" data-rel="back" id="save-activity-button" class="ui-btn-right">Save</a>
        <h1>New Activity</h1>
      </div>
      <div data-role="content" id="activity-form-content">
          <!-- the contents of the form will be rendered via the backbone view -->
          <form id="activity-form-form" action="#"></form>
      </div>
    </div>

In the code above, we see the typical jQueryMobile page. The main things of interest here are with respect to the anchor tag in the header div:

  • Specify data-rel=”back” to navigate to the previous page once the save is complete. This way whether we enter the form page from “Add” or “Edit”, navigation will be handled.
  • The class=”ui-btn-right” moves the button to the right side of the header bar.
  • The data-theme=”b” causes the save button to use the blue theme.

The next step is to define the template that the view will use to render the content for this page. Lets start with a simple template with an input for each attribute of an activity. To vary the UI slightly, we can use a drop down for the activity type. The basic template will look like this:

    <script type="text/template" id="activity-form-template">
        <label for="date" class="select">Date</label>
        <input type="date" name="date" id="date" placeholder="Date" value="<%= date %>" />
        
        <label for="type" class="select">Type</label>
        <select name="type" id="type">
            <option>Run</option>
            <option>Bike</option>
            <option>Swim</option>
            <option>Walk</option>
        </select>
        
        <label for="distance">Distance</label>
        <input type="text" name="distance" id="distance" placeholder="" value="<%= distance %>" />
    
        <label for="minutes">Minutes</label>
        <input type="tel" name="minutes" id="minutes" placeholder="" value="<%= minutes %>" />
        
        <div data-role="fieldcontain">
            <textarea name="comments" id="comments" placeholder="Comments"><%= comments %></textarea>
        </div>
    </script>

Notice that we are using the HTML 5 input types for some of the fields. Browser support for these will vary, but worst case they will deprecate to a text input type. For example, on iOS, the date will present a native date picker, whereas on Android (or Chrome, etc.) the field will behave as a standard text input.

Now we need to navigate to the form page. This requires modifying the “Add” button on the list page.

    <div data-role="page" id="activities">
      <div data-role="header">
        <h1>Activities</h1>
        <a href="#activity-form" data-role="button" data-icon="add" id="add-button" class="ui-btn-right">Add</a>
      </div>
      <div data-role="content">
          <!-- the contents of the list view will be rendered via the backbone view -->
      </div>
    </div>

In order to transition to the form page, the href attribute on the anchor tag (i.e. button) needs to be changed to the id of the form page, as shown in line 4 above. The final step in presenting the form page to support adding exercise activities is modifying the add button click handler.

    $('#add-button').live('click', function(){
        var activity = new exercise.Activity(),
            activityForm = $('#activity-form-form'),
            activityFormView;

        activityFormView = new exercise.ActivityFormView({model: activity, viewContainer: activityForm});
        activityFormView.render();
    });

Here we create an empty model, grab the form node, create a new exercise.ActivityFormView with these objects, and render the form. Testing at this point reveals a problem. The screen shot below doesn’t look right ūüėČ

Investigating the browsers console reveals an error: Uncaught ReferenceError: distance is not defined. This is due to the underscore template trying to access an undefined attribute of the model. The issue is described here with some work arounds. I found the easiest workaround is to provide defaults for the model, as shown below.

     exercise.Activity = Backbone.Model.extend({
        defaults: {
            date: '',
            type: '',
            distance: '',
            comments: '',
            minutes: ''
        }
     });

Now the form renders properly. With the help of some CSS, we can tighten things up a little and end up with the version on the right (the CSS is part of the source code, see the links at the end of this post).

Editing an existing activity

With the template and the view already implemented, adding edit functionality is pretty straightforward. The first step is to update the href attribute of the edit button on the activity-details page.

    <div data-role="page" id="activity-details" data-add-back-btn="true">
      <div data-role="header">
        <a href="#activity-form" data-role="button" data-icon="arrow-d" id="edit-activity-button" class="ui-btn-right">Edit</a>
        <h1>Activity Details</h1>
      </div>
      <div data-role="content" id="activity-details-content">
          <!-- the contents of the list view will be rendered via the backbone view -->
      </div>
    </div>

As we did with the add button, this should reference the activity-form page. Now we need to add the click event handler for the edit button.

    $('#edit-activity-button').live('click', function() {
        var activityId = $('#activity-details').jqmData('activityId'),
            activityModel = exercise.activities.get(activityId),
            activityForm = $('#activity-form-form'),
            activityFormView;
        
        activityFormView = new exercise.ActivityFormView({model: activityModel, viewContainer: activityForm});
        activityFormView.render();
    });

If you recall, the activityId is passed to the details page view the list item click event handler. We reuse that fact here to retrieve the activity from the collection.

Time to test. Selecting an activity renders the detail page. Clicking the edit button renders the form, pre-filled with the activity details. Everything looks great.

But wait…..that screenshot was from the browser on which I am testing. We should always test on devices as well. Earlier I mentioned that we are using some of the HTML 5 input types (i.e. date). The nice thing is that iOS 5 will render a nice date picker for date types. The challenge is that the date needs to be in a specific format for that to work. The image below shows what the form page looks like on an iOS device.

Fixing the Date

No date value is showing up on our iOS device. This is a known issue. So, for iOS we need the date format as yyyy-mm-dd, while for other platforms, I prefer the date to be shown as mm/dd/yyyy. I do realize that date formats should be localized, but for the purpose of this example I would like to keep it straightforward and specify mm/dd/yyyy as the display date format. There are many options that could be pursued here, but to demonstrate some more capabilities of Backbone.js, we will modify our model to help meet our date requirements.

First, the date attribute in our JSON feed is a string. Lets convert this to a date when the data is retrieved from the server, then when we need the date, we can just manipulate it as needed. This can be accomplished several ways.

  • Implement a parse method on the collection that converts the String to a Date as the data is being fetched from the server.
  • Implement a method on the model that converts the date String to a Date and sets it on the model. This would then require the calling code to know to call this method depending upon the circumstance.
  • Override the set method on the model, look for the date attribute, then convert it to a date as needed.

The last option is the most seamless approach. Add the following method to the Activity model.

        set: function(attributes, options) {
            var aDate;
            if (attributes.date){
                //TODO future version - make sure date is valid format during input
                aDate = new Date(attributes.date);
                if ( Object.prototype.toString.call(aDate) === "[object Date]" && !isNaN(aDate.getTime()) ){
                    attributes.date = aDate;
                }
            }
            Backbone.Model.prototype.set.call(this, attributes, options);
        }

Also, update the defaults so that the date attribute is now a Date instead of a String.

        defaults: {
            date: new Date(),
            type: '',
            distance: '',
            comments: '',
            minutes: ''
        },

While this clearly isn’t the most robust date handling, it is fine for now. In a future version, this should be improved.

Next, add attributes to our model that will format the date in the ways we need it (i.e. mm/dd/yyyy and yyyy-mm-dd).

        dateInputType: function(){
            return exercise.formatDate(this.get('date'), "yyyy-mm-dd"); //https://github.com/jquery/jquery-mobile/issues/2755
        },
        
        displayDate: function(){
            return exercise.formatDate(this.get('date'), "mm/dd/yyyy");
        }

The formatDate function is a simple date formatter to meet our specific needs and can be found in the source code that accompanied this post (see link at the bottom of the post).

Now, how do we use these new methods in our view. The first thing to realize is that we pass the template JSON. The default implementation of the toJSON method of a Backbone.js model will not include these functions. Therefore, we need to override the toJSON method.

        toJSON: function(){
            var json = Backbone.Model.prototype.toJSON.call(this);
            return _.extend(json, {dateInputType : this.dateInputType(), displayDate: this.displayDate()});
        }

Here, we are using the Underscore.js extend to add our attributes to the standard Backbone JSON. Now, we need to modify our view templates to use the appropriate JSON attributes.

    <script type="text/template" id="activity-list-item-template">    
      <li><a href="#activity-details" identifier="<%= id %>"><%= displayDate %> - <%= type %></a></li>
    </script>
    
    <script type="text/template" id="activity-details-template">
        <h3><%= type %></h3>
        <ul data-role="listview" id="activitiy-fields" data-inset="true">
          <li>Date: <%= displayDate %></li>
          <li>Minutes: <%= minutes %></li>
          <li>Distance: <%= distance %></li>
          <li>Comments: <%= comments %></li>
        </ul>
    </script>
    
    <script type="text/template" id="activity-form-template">
        <label for="date" class="select">Date</label>
        <% if (navigator.userAgent.indexOf('iPhone') >= 0 || navigator.userAgent.indexOf('iPad') >= 0) { %>
            <input type="date" name="date" id="date" placeholder="Date" value="<%= dateInputType %>" />
        <% }else{ %>
            <input type="date" name="date" id="date" placeholder="Date" value="<%= displayDate %>" />
        <% } %>
        
        <label for="type" class="select">Type</label>
        <select name="type" id="type">
            <option>Run</option>
            <option>Bike</option>
            <option>Swim</option>
            <option>Walk</option>
        </select>
        
        <label for="distance">Distance</label>
        <input type="text" name="distance" id="distance" placeholder="" value="<%= distance %>" />
    
        <label for="minutes">Minutes</label>
        <input type="tel" name="minutes" id="minutes" placeholder="" value="<%= minutes %>" />
        
        <div data-role="fieldcontain">
            <textarea name="comments" id="comments" placeholder="Comments"><%= comments %></textarea>
        </div>
    </script>

Notice that lines 16-21 include some conditional logic. This is a very basic device detection check to determine which date format to use. There are plug-ins,etc. that provide more robust alternatives, but to keep things clear, this will serve our needs. The screen shots below show the desktop browser and the iOS versions of the form with the appropriate date handling.

Saving

The final step is to implement the save functionality.

    $('#save-activity-button').live('click', function(){
        var activityId = $('#activity-details').jqmData('activityId'),
            activity,
            dateComponents,
            formJSON = $('#activity-form-form').formParams();
        
        //if we are on iOS and we have a date...convert it from yyyy-mm-dd back to mm/dd/yyyy
        //TODO future version - for non-iOS, we would need to validate the date is in the expected format (mm/dd/yyyy)
        if (formJSON.date && ((navigator.userAgent.indexOf('iPhone') >= 0 || navigator.userAgent.indexOf('iPad') >= 0)) ){
            dateComponents = formJSON.date.split("-");
            formJSON.date = dateComponents[1] + "/" + dateComponents[2] + "/" + dateComponents[0];
        }
        
        if (activityId){
            //editing
            activity = exercise.activities.get(activityId);
            activity.set(formJSON); //not calling save since we have no REST backend...save in memory
        }else{
            //new (since we have no REST backend, create a new model and add to collection to prevent Backbone making REST calls)
            activity = new exercise.Activity(formJSON);
            activity.set({'id': new Date().getTime()});  //create some identifier
            exercise.activities.add(activity);
        }
    });

I used the JavascriptMVC formParams jQuery plugin (found here) to convert my HTML form into a JSON object. Then, the date is converted into the appropriate format. One thing to note here is that since we have no real back end server for Backbone to interface with, we don’t call save on the model or create on the collection. These methods would cause the appropriate REST calls to the server.

Since the click event on the list view passes the activityId to the details page, we can use this to determine if we are adding or editing. One thing to consider here is that the user may click an existing activity, then go back to the list, then click add. In this case, the activityId from the previously selected activity is still attached to the activity-details view. This will cause our save implementation to think we are editing. To prevent this, we should remove the activityId from the activity-details page when adding a new activity. This can be accomplished by adding the highlighted line below to the add handler.

    $('#add-button').live('click', function(){
        var activity = new exercise.Activity(),
            activityForm = $('#activity-form-form'),
            activityFormView;
    
        //clear any existing id attribute from the form page
        $('#activity-details').jqmRemoveData('activityId');
        activityFormView = new exercise.ActivityFormView({model: activity, viewContainer: activityForm});
        activityFormView.render();
    });

The final step is to make sure our views are updated accordingly when we add or modify activities. Since the details view retrieves the activity before the page loads, no change is required here. But our list view only renders during the initial page load. This is where we can take advantage of Backbone event binding. A few small additions to the ActivityListView and things will be handled.

        initialize: function() {
            this.collection.bind('add', this.render, this);
            this.collection.bind('change', this.changeItem, this);
            this.collection.bind('reset', this.render, this);
            this.template = _.template($('#activity-list-item-template').html());
        },

        ...

        changeItem: function(item){
            this.collection.sort();
        }

The change event will bubble up from the model and fires as a result of the set being called. In the changeItem method, we sort the collection to handle any changes in the date attribute. The sort method call will cause the reset event to fire, which then results in the render method invocation, causing the list to be re-rendered. This keeps everything in the correct order. The add event will be fired when we add a new activity to the collection. Since we implemented the comparator method on the collection, models added to the collection will be added in the appropriate order.

We can now add and edit activities. Start exercising ūüôā

The source code for this post can be found here

From a List to a Detail View using jQuery Mobile and Backbone.js

In my previous post I built a basic application to demonstrate the use of Backbone.js with jQueryMobile (JQM). The introduction can be found here, with a brief subsequent post on sorting collections here. In this post, I would like to add the capability to view the details of the items presented in the list view.

The first step is to create a new JQM page to display the details view. JQM makes it pretty easy to add pages to your application. I added the following code to the index.html file:

<div data-role="page" id="activity-details" data-add-back-btn="true">
  <div data-role="header">
    <a href="#" data-role="button" data-icon="arrow-d" id="edit-activity-button" class="ui-btn-right">Edit</a>
    <h1>Activity Details</h1>
  </div>
  <div data-role="content" id="activity-details-content">
      <!-- the contents of the list view will be rendered via the backbone view -->
  </div>
</div>

This will create the structure for the page, and Backbone.js will be used to fill in the content div based on the record tapped (or clicked) within the list view. The next step is to define the template for the details view. This is the pattern that I follow when developing using jQueryMobile and Backbone.js. The template can be inserted below the list item template in index.html.

    <script type="text/template" id="activity-details-template">    
        <h3><%= type %></h3>
        <ul data-role="listview" id="activitiy-fields" data-inset="true">
          <li>Date: <%= date %></li>
          <li>Minutes: <%= minutes %></li>
          <li>Distance: <%= distance %></li>
          <li>Comments: <%= comments %></li>
        </ul>
    </script>

I decided to embed the details in a read-only list view, this way jQueryMobile will provide some reasonable styling. Since the point here is to demonstrate Backbone and jQueryMobile playing together, I didn’t want to have to spend much time on style ūüôā

Next, we need to define the Backbone view that will use the template to render the appropriate content. All this View needs to do is apply the model to the template and append it to the HTML container defined when the View is instantiated.

    exercise.ActivityDetailsView = Backbone.View.extend({
        //since this template will render inside a div, we don't need to specify a tagname
        initialize: function() {
            this.template = _.template($('#activity-details-template').html());
        },
        
        render: function() {
            var container = this.options.viewContainer,
                activity = this.model,
                renderedContent = this.template(this.model.toJSON());
                
            container.html(renderedContent);
            container.trigger('create');
            return this;
        }
    });

In order to retrieve the correct model to bind to the details view, we need to know what row in the list view was clicked (or tapped). To do this, we can bind to the click event of the item in the list view. This can be accomplished by modifying the ActivityListView render method. Here is the current version of the list view:

    exercise.ActivityListView = Backbone.View.extend({
        tagName: 'ul',
        id: 'activities-list',
        attributes: {"data-role": 'listview'},
        
        initialize: function() {
            this.collection.bind('add', this.add, this);
            this.template = _.template($('#activity-list-item-template').html());
        },
        
        render: function() {
            var container = this.options.viewContainer,
                activities = this.collection,
                template = this.template,
                listView = $(this.el);
                
            $(this.el).empty();
            activities.each(function(activity){
                listView.append(template(activity.toJSON()));
            });
            container.html($(this.el));
            container.trigger('create');
            return this;
        },
        
        add: function(item) {
            var activitiesList = $('#activities-list'),
                template = this.template;
                
            activitiesList.append(template(item.toJSON()));
            activitiesList.listview('refresh');
        }
    });

The key area to focus on here is lines 18-20, where the activity item HTML is rendered and appended to the list view. This is where the modification needs to occur. Each activity item HTML element needs to be bound to a click event. In this click event, the activity id will need to somehow be passed to the details view so the appropriate look up can occur. There are several ways to do this. Approaches I have used in the past include the use of jQuery.jqmData(...) or session local storage (assuming HTML5). In this example, we will use the jqmData method. The trick is to capture the id during the rendering of the list so that it can be used during the execution of the click event. Below are the required modifications to lines 18-20 of the previous code snippet.

activities.each(function(activity){
   var renderedItem = template(activity.toJSON()),
       $renderedItem = $(renderedItem);  //convert the html into an jQuery object
   $renderedItem.jqmData('activityId', activity.get('id'));  //set the data on it for use in the click event
   $renderedItem.bind('click', function(){
         //set the activity id on the page element for use in the details pagebeforeshow event
        $('#activity-details').jqmData('activityId', $(this).jqmData('activityId'));  //'this' represents the element being clicked
   });
   listView.append($renderedItem);
});

The first thing is to capture the rendered activity item HTML in a variable and cast it to a jQuery object, as can be seen in lines 2-3. Line 4 is where the activity id data is attached to our activity item HTML element. Then, the bind event (lines 5-8) retrieves the attached data and sets it on the activity details HTML element, which is our activity details page HTML. Per the jQuery documentation, this within a bind method refers to the DOM element to which the event handler is bound. We can use that to get at the activity id data and attach it to the details view. This gives us the ability to pass the appropriate id at runtime to the details page.

Now we need to wire all this together. The typical pattern I follow is to use the jQueryMobile pagebeforeshow event to set up everything needed to render a complete page. This acts as my controller.

$('#activity-details').live('pagebeforeshow', function(){
    var activitiesDetailsContainer = $('#activity-details').find(":jqmData(role='content')"),
        activityDetailsView,
        activityId = $('#activity-details').jqmData('activityId'),
        activityModel = exercise.activities.get(activityId);
    
    activityDetailsView = new exercise.ActivityDetailsView({model: activityModel, viewContainer: activitiesDetailsContainer});
    activityDetailsView.render();
});

This code retrieves the id data attached to the activity details element, looks up the model using the Backbone API (line 5), instantiates the view with the model and the view container, and calls the render method to render the final HTML. The end result looks like this:

The source code for this post can be found here. Note that this is a branch of my repository for this sample app. Each blog post associated with this code base resides on a separate branch.

Related Posts:

This is a re-post of my original found here.

Basic Mobile Device Detection

These days, there are many ways to browse the internet.  There are desktop browsers, mobile phone browsers, tablets, tablet/phones (e.g. Dell Streak), etc. all with different screen sizes, resolution, and capabilities.  How do you render an appropriate view optimized for the device accessing your site?  The answer is device detection.

The two main approaches for device detection are server-side detection and client side detection.  Server side detection involves determining the device at the time of the request and then presenting the appropriate view.  Client side detection involves manipulating the view after the content has been sent to the browser.

Client side approaches include: feature detection via JavaScript, CSS media types, and CSS media queries.  This approach, while simpler, is subject to the capabilities of the rendering browser.  Some browsers have little or no support for JavaScript, while others only support older CSS standards.  With the myriad of devices and their associated browsers, this can be a significant challenge.

When it comes to server-side detection, the main approach is to rely on the USER_AGENT (UA) HTTP header string that comes in each web request.  There are many frameworks and tools to help interpret the UA string, and I will present some of those options in a subsequent blog post.  For now, it is a safe assumption that classifications of devices (e.g smart phone, feature phone, etc.) can be determined via the UA string.

There are many ways to use one or both of these approaches.  As always, it depends upon your requirements.  Many times, a simple approach will work best.  WebKit based browsers, such as those found on iOS, Android, and some Blackberry devices, are the most capable mobile browsers today.  These devices have excellent support for JavaScript and CSS.  It may be perfectly acceptable to focus on devices with these browsers and render a touch based, optimized experience for those requests, while presenting a basic HTML only view for any other mobile devices.  Using server-side detection, the browser type can be determined and the appropriate content can be sent to the browser for rendering.

Is that enough? ¬†Today’s mobile landscape is rapidly changing, with new phones and tablets coming out each month. ¬†All these devices have different screen sizes, resolutions, etc. ¬†Is one mobile touch based user interface design enough? ¬†Would the same view be appropriate on a 3.5 inch iPhone screen as a Galaxy Tab 7 inch screen? ¬†What about a 9.7 inch iPad screen? ¬†Probably not. ¬†This is where client side detection can come in handy. ¬†Through the use of CSS and JavaScript, content can be altered, adapted, and enhanced to take advantage of the larger screens when appropriate.

While this combination of server and client side detection may not be appropriate for your circumstances, it is a simple approach that is valid for many situations.