Friday, July 06, 2012

Code examples for a Dojo Mobile one page application. Backend in Ruby/Sinatra

I wrote a few days ago that I am excited about how easy it is to make simple one page web apps using Dojo Mobile that look good and work fine on portable devices (Android and iOS) and regular web browsers. I don't do a lot of UI development for my work but when I do write web apps it is great to also be able to support mobile devices with a small amount of additional simple code. In this post I will show you hopefully useful code snippets for cookingspace.com that may save you some time if you want to write the same type of apps.

Last weekend I decided that I wanted a new mobile web interface to my old Cooking Space web site. I wanted to be able to quickly look up a recipe on my cellphone, see the ingredient list, and be able to specify how many people need to be served. I also want to see the nutrition data for the recipe. A top level requirement is that once the web page renders then everything is updated with AJAX. There are only two user interactions:

  • Enter a few search terms to locate recipes and show them in a format compatible with both small devices and a web browser on a laptop.
  • A control for changing the number of people a recipe will serve - this adjusts the amounts in the ingredients list using AJAX.
This is much simpler UI than that for the original web site but captures just what I personally want to use it for. I use the admin functionality on the old web site for adding and editing recipes which is a complex task because I use the USDA nutrition database and instead of for example adding "chicken" to a recipe I need to find out of the 200 entries for chicken in the nutrient database which entry for chicken to use. Same for all ingredients. This admin functionality is not user facing code but five years ago I implemented it in Rails with a nice AJAX enabled UI.

There is a lot of backend Ruby model code that I won't show. I'll show the controller code, the Javascript, and the HTML5 and Dojo application code I used.

The Sinatra controller code is very simple since it only has to serve up the web page and then handle AJAX calls using the model code. I reformatted the Ruby code to make it a little more verbose and easier to understand:

enable :sessions

get '/' do
  session["n"] ||= "4"
  erb :index
end

get '/getr' do  # handles AJAX calls from Javascript
  ret = "No recipes found"
  q = params['q']
  n = params['n']
  session["n"] = n.to_s   if n
  if q && q.length > 0
    ids = search q # calls model code to do search
    if ids && ids.size > 0
      num_served = session["n"].to_i
      ret = recipes_to_html num_served,ids.collect {|x| x[0]}
    end
  end
  ((!q || q.length == 0) && n) ? "*" : ret
end
The handler for the route '/getr' uses two parameters for search terms and number of people to be served. The web page has two sections: the top section contains the search input form and a control to set the number of people servered. The bottom section just contains an HTML element that receives the AJAX response. I usually put Javascript in a separate file but the Javascript code for this app is so short and simple I just included it in the HTML/ERB template file. Much of the following Dojo boilerplate comes from the excellent Dojo Mobile Documentation:
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <title>CookingSpace</title>
  <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.7.1/dojo/dojo.js"
         data-dojo-config="async:false"></script>
 <script>
    require(["dojox/mobile/parser",
             "dojox/mobile/deviceTheme",
             "dojox/mobile/compat",
             "dojox/mobile",
             "dojox/mobile/TextBox"],
        function(parser, deviceTheme) {
            parser.parse();
        }
     );

     function ajax_helper() {
       dojo.xhrGet({
        url: "/getr?q=" + dojo.byId("q").value +
                     "&n=" + dojo.byId("served").value,
        handleAs:"text",
        timeout: 8000, // 8 seconds
        load: function(data, args) {
          if (data != "*") {
            dojo.byId("rcontent").innerHTML = data;
          }
        },
        error: function(err, args){
          dojo.byId("rcontent").innerHTML =
           "An unexpected error occurred: " + err;
        }
     });
    }
    dojo.connect(dojo.byId("q"), 'onchange', ajax_helper);
   </script>
  </head>
  <body>
    <div id="settings"
         data-dojo-type="dojox.mobile.View"
         data-dojo-props="selected: true">
    <h3 data-dojo-type="dojox.mobile.Heading">
      CookingSpace Mobile Recipe Lookup
    </h3>
    <div data-dojo-type="dojox.mobile.RoundRect"
         data-dojo-props='shadow:true'>
    <input id="q" name="q" size="40"
           data-dojo-type="dojox.mobile.TextBox"
           placeHolder="Search for recipes" />

    Number served:
    <select name="served" id="served">
      <option value="1">1 person</option>
      <option value="2" selected="selected">2 people</option>
      <option value="3">3 people</option>
      <option value="4">4 people</option>
      <option value="5">5 people</option>
      <option value="6">6 people</option>
    </select>
   </div>

 <div id="rcontent" data-dojo-type="dojox.mobile.RoundRect">
 ...
 </div
 
Please note that I set asynchronous loading of Dojo resources to false to make sure that the Dojo parser had a chance to parse the HTML on this page marking elements with Dojo types before processing my Javascript. This simplest approach is OK for a one page web app that only gets loaded once.

This is a simple example and hopefully will give you a good start creating one page mobile HTML5 web apps using Dojo Mobile.

2 comments:

John Umeh said...

Thanks, very helpful! Also, your code where not well formatted while viewing from my phone, but I guess that's a fault of blogger.

Mark Watson, author and consultant said...

Hello John, I try to narrow the width of code listings to look OK on a web browser, but not so easy for a smartphone :-)