Friday, April 22, 2011

Adding Rich Dynamic Data to jQuery Mobile Apps

In my first post we added simple dynamic content to a jQuery Mobile (jqm) app and in the second we secured the content with Spring Security. In this post we will look at displaying richer dynamic content. Working with jqm we have several options. First we could return complete pages from the server that jqm could load and swap out via ajax. Another option would be to return parts of the page as HTML content that we could swap into the pages. The last option that we will implement is the server returning json content that we will use to create dynamic views. The biggest influence on this decision was that I want to build a app that can also be published in native apps using PhoneGap. By returning too much (good luck quantifying that) HTML content, Apple will get worried that you will make changes to your app without getting it re-approved and smite your application. The other main advantage of transmitting content as json is to minimize the size of the transmitted content on a page request.
First we will update our dynamic data controller method to return a set of books.
@RequestMapping(value="/dynamicData", method=RequestMethod.GET)
public @ResponseBody List getDynamicData(Model model){
  List books = new ArrayList();
  Book book = new Book();
  book.setId(1l);
  book.setTitle("The Road to Serfdom");
  book.setAuthor("Friedrich Hayek");
  books.add(book);
  book = new Book();
  book.setId(2l);
  book.setTitle("Capitalism and Freedom");
  book.setAuthor("Milton Friedman");
  books.add(book);
  book = new Book();
  book.setId(3l);
  book.setTitle("The General Theory");
  book.setAuthor("John Keynes");
  books.add(book);
  return books;
}

private class Book {

  private Long id;
  private String title;
  private String author;
  
  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getAuthor() {
    return author;
  }
  public void setAuthor(String author) {
    this.author = author;
  }
}
Now that out ajax call will return the list we have two options for displaying this data to the user. The first would be programmatically generating html from the json content. The second would be to use a javascript templeting plugin. By utilizing a templeting plugin we will have more concise code that is easier to maintain. This demo will use the recently added top level jQuery template plugin, currently in beta.
With the goal of displaying the books in a standard jqm styled list the following simple template was created. It will style title and author and add the books id as a data element that will be helpful for onclick methods.
<script id="bookTemplate" type="text/x-jquery-tmpl">
  <li data-id=${id} class='bookButton'>
 <h3>${title}</h3>
 <p>${author}</p>
  </li>
</script>
As this template will be used repetitively we will precompile it to a named template. This should speed up execution time. See http://boedesign.com/misc/presentation-jquery-tmpl/#13.
$('#bookTemplate').template('bookTemplate');
Next we will update bar with the shell of a jqm list to hold the data.
<div id="bar" data-role="page">
  <div data-role="header">
 <h1>Bar</h1>
  </div>
  <div data-role="content"> 
 <ul data-role="listview" id="dynamicContentHolder">
 </ul>
 <br/>
 <a href="#foo" data-inline="true" data-role="button" >To foo</a>
 <a id="buttonSignOut" name="buttonSignOut" href="#" data-role="button" data-inline="true">Sign Out</a>
  </div>
</div>
Finally we will update the ajax call to parse the json response content, generate html content with the template, and insert the content into bar’s jqm list. Finally we will refresh the listview to allow jqm to apply appropriate styling to the content.
$('#bar').live('pagebeforeshow',function(event, ui){
  $.mobile.pageLoading();
  $('#dynamicContentHolder').text('');
  var dynamicDataResp = $.ajax({
        url: "/dynamicData.json",
        dataType: 'json',
        async: false,
        cache: false            
      });
  if(dynamicDataResp.status == 200){
    var dynamicDataObj = jQuery.parseJSON(dynamicDataResp.responseText);
    $.tmpl('bookTemplate', dynamicDataObj).appendTo('#dynamicContentHolder');
    $('#dynamicContentHolder').listview('refresh');
  }
  ...
The next thing I wanted to be able to do, is control navigation and pass information on the selected book when a user selects one off the list. So let’s add a new page for this content.
<div id="bookDetails" data-role="page">
  <div data-role="header">
 <h1>book details</h1>
  </div>
  <div data-role="content">
 <span id="bookContent"/>
  </div>
</div>
Now as we included the class bookButton on each of our book list items we can register a click handler that will query the id from the element, save it as context information to the page, and navigate to the details page.
$('.bookButton').live('click', function() {
  var bookId = $(this).jqmData('id');
  $('#bookDetails').jqmData('bookId', bookId);
  $.mobile.changePage('#bookDetails');
});
Finally on bookDetail's pagebeforeshow event we can alter the page databased on the bookId metadata stored to the page. The example below simply prints out the info, but using the skills you have learned so far you could easily issue a query here to retrieve additional information on the book.
$('#bookDetails').live('pagebeforeshow', function(event, ui){
  var bookId = $('#bookDetails').jqmData('bookId');
  if(bookId != null){
    $('#bookContent').text("Details about book #" + bookId + " here.");
  }
  else{
    $.mobile.changePage('#error', 'none', false, false);
  }
});
As usual here is the complete jqm app.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1">
 <title>App Name</title>
 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a4.1/jquery.mobile-1.0a4.1.min.css" />
 <script type="text/javascript" src="http://code.jquery.com/jquery-1.5.2.min.js"></script>
 <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
 <script id="bookTemplate" type="text/x-jquery-tmpl">
      <li data-id=${id} class='bookButton'>
        <h3>${title}</h3>
        <p>${author}</p>
      </li>
    </script> 
 <script type="text/javascript">
 $('#bookTemplate').template('bookTemplate');
    
 $(document).bind("mobileinit", function(){
  
  $('.bookButton').live('click', function() {
   var bookId = $(this).jqmData('id');
   $('#bookDetails').jqmData('bookId', bookId);
   $.mobile.changePage('#bookDetails');
    });
    
   $('#bookDetails').live('pagebeforeshow', function(event, ui){
  var bookId = $('#bookDetails').jqmData('bookId');
  if(bookId != null){
    $('#bookContent').text("Details about book #" + bookId + " here.");
  }
  else{
    $.mobile.changePage('#error', 'none', false, false);
  }
   });
  
  $('#buttonSignOut').live("click", function() {
   $.mobile.pageLoading(); 
   $.ajax({
    url: '/signout',
    complete: function(transport) {
     if(transport.status == 200) {
       $.mobile.changePage('#foo');
      } else {
       $.mobile.changePage('#error', 'none', false, false);
      }
    }
   });
        return false;      
      });
   $('#signInForm').live("submit", function() {
  $.mobile.pageLoading(); 
     // Submit the form
  $.ajax({
   type: 'POST',
   url: '/signin/authenticate',
   data: $('#signInForm').serialize(),
   complete: function(transport) {
    if(transport.status == 200) {
     history.back();
    } else {
     $('#loginError').show();
     $.mobile.pageLoading(true);
    }
      }
  });
  return false;
   });
   
   $('#login').live('pagebeforeshow', function(event, ui){
  $('#loginError').hide();
   });
 
 
  $('#bar').live('pagebeforeshow',function(event, ui){
   $.mobile.pageLoading();
   $('#dynamicContentHolder').text('');
   var dynamicDataResp = $.ajax({
       url: "/dynamicData.json",
       dataType: 'json',
       async: false,
                      cache: false       
     });
   if(dynamicDataResp.status == 200){
    var dynamicDataObj = jQuery.parseJSON(dynamicDataResp.responseText);
    $.tmpl('bookTemplate', dynamicDataObj).appendTo('#dynamicContentHolder');
    $('#dynamicContentHolder').listview('refresh');
   }
   else if(dynamicDataResp.status == 401){
    $.mobile.changePage('#login');
    return false;
   }
   else{
    $.mobile.changePage('#error', 'none', false, false);
   }
  });
 });
 </script>
 <script type="text/javascript" src="http://code.jquery.com/mobile/1.0a4.1/jquery.mobile-1.0a4.1.min.js"></script>
 
  </head> 
  <body> 
    <div id="foo" data-role="page">
      <div data-role="header">
  <h1>Foo</h1>
   </div>
   <div data-role="content"> 
  <h2>Foo</h2>
  <a href="#bar">To bar</a>
   </div>
   
    </div>
    <div id="bar" data-role="page">
      <div data-role="header">
  <h1>Bar</h1>
   </div>
   <div data-role="content"> 
  <ul data-role="listview" id="dynamicContentHolder">
  </ul>
  <br/>
  <a href="#foo" data-inline="true" data-role="button" >To foo</a>
  <a id="buttonSignOut" name="buttonSignOut" href="#" data-role="button" data-inline="true">Sign Out</a>
   </div>
    </div>
 <div id="bookDetails" data-role="page">
   <div data-role="header">
  <h1>book details</h1>
   </div>
   <div data-role="content">
     <span id="bookContent"/>
   </div>
 </div>
    <div id="login" data-role="page">
      <div data-role="header">
  <h1>Login</h1>
   </div>
   <div data-role="content">
  <span id="loginError" class="error">Your credentials are invalid. Please try again.</span>
     <form id="signInForm" data-ajax="false">
       <div data-role="fieldcontain">
            <label for="login">Username or Email</label>
            <input id="login" name="j_username" type="text" size="25" autocorrect="off" autocapitalize="off" />
          </div>
     <div data-role="fieldcontain">
    <label for="password">Password</label>
    <input id="password" name="j_password" type="password" size="25" /> 
   </div>
   <div id="submitDiv" data-role="fieldcontain">    
    <input type="submit" value="login" data-inline="true"/>
   </div>
     </form> 
   </div>
    </div>
 <div id="error" data-role="page">
      <div data-role="header">
  <h1>Error</h1>
   </div>
   <div data-role="content"> 
  Click back to return to the previous page.
   </div>
    </div>
  </body>
</html>

1 comment:

Anonymous said...

Thank you for the informative port.
It helped me understand the concept.
Amnon