After my post yesterday - I decided to extract out an example app from my current work.

A backbone mobile app running in Safari

You can try the app in your browser, or view the source code on github.

Technology

I’m using the Foursquare API, Backbone.js, Underscore.js, CoffeeScript, jQuery and jQuery Mobile. The application is written in CoffeeScript - if you make changes to application.coffee - you will need to rerun coffeescript to recompile application.js.

If you aren’t up to date with coffeescript you can probably read through application.js and understand most of the code. I also cached a foursquare venue API search result as foursquare.json. The foursquare API doesn’t support JSONP so I couldn’t use it directly.

Patch to jQuery Mobile

There is only patch require to the libraries - one that I discussed in my previous post to make jQuery route the URLs correctly.

Index.html

The index.html has very little in it, just the outline HTML for the first page to be displayed. The actual page content is generated by HomeView in application.coffee. If you haven’t seen the data-role stuff before, I recommend you read through the jquery mobile docs.

Application.coffee

Pretty much the entire application is written in application.coffee.

app

A namespace with a few helpers I use to work with jquery mobile better. activePage returns the currently displayed page. reapplyStyles make jQuery mobile convert elements tagged with data-role=”whatever” into correctly displayed jquery mobile widgets. The next version of jQuery mobile might fix the .page() method, and so reapplyStyles can be removed.

Venue class

class Venue extends Backbone.Model
  getName: ->
    @get('name')

  getAddress: ->
    [@get('address'), @get('city'), @get('state')].join ", "

  getImageUrl: ->
    @get('photo_url')

  getLatitude: ->
    @get('geolat')

  getLongitude: ->
    @get('geolong')

  getMapUrl: (width, height) ->
    width ||= 300
    height ||= 220

    "http://maps.google.com/maps/api/staticmap?center=#{@getLatitude()},#{@getLongitude()}&zoom=14&size=#{width}x#{height}&maptype=terrain&markers=color:red|#{@getLatitude()},#{@getLongitude()}&sensor=false"

This is a backbone model that takes a hash of data and makes it available with a few helper methods.

VenueCollection

class VenueCollection extends Backbone.Collection
  model : Venue
  
  constructor: ->
    @refresh($FOURSQUARE_JSON)

This is instantiated as Venues (try running Venues.models in your javascript console and you’ll see all the loaded venues). When the collection is created, I call refresh with the FOURSQUARE_JSON (which is loaded from foursquare.js) which populates the collection immediately.

EditVenueView

class EditVenueView extends Backbone.View
  constructor: ->
    super

    # Get the active page from jquery mobile. We need to keep track of what this
    # dom element is so that we can refresh the page when the page is no longer active.
    @el = app.activePage()

    @template = _.template('''
    <form action="#venue-<%= venue.cid %>-update" method="post">

      <div data-role="fieldcontain">
        <label>Name</label>
        <input type="text" value="<%= venue.getName() %>" name="name" />
      </div>
  
      <div data-role="fieldcontain">
        <label>Address</label>
        <input type="text" value="<%= venue.get('address') %>" name="address" />
      </div>
  
      <div data-role="fieldcontain">
        <label>City</label>
        <input type="text" value="<%= venue.get('city') %>" name="city" />
      </div>
  
      <div data-role="fieldcontain">
        <label>State</label>
        <input type="text" value="<%= venue.get('state') %>" name="state" />
      </div>
  
      <button type="submit" data-role="button">Save</button>
    </form>
    ''')

    # Watch for changes to the model and redraw the view
    @model.bind 'change', @render

    # Draw the view
    @render()

  events : {
    "submit form" : "onSubmit"
  }

  onSubmit: (e) ->
    @model.set {
      name : @$("input[name='name']").val(),
      address : @$("input[name='address']").val(),
      city : @$("input[name='city']").val(),
      state : @$("input[name='state']").val()
    }

    @model.trigger('change')

    app.goBack()

    e.preventDefault()
    e.stopPropagation()

  render: =>
    # Set the name of the page
    @el.find('h1').text("Editing #{@model.getName()}")

    # Render the content
    @el.find('.ui-content').html(@template({venue : @model}))

    # A hacky way of reapplying the jquery mobile styles
    app.reapplyStyles(@el)

    # Delegate from the events hash
    @delegateEvents()

The first view. It sets the active page to the @el attribute, so that it can refresh the page later on (remember that the activePage may change when the user navigates to another page, so we need to be able to refer to what element we drew the form in). Then we create a template using underscore.template. The underscore template library uses ERB style syntax to generate HTML. Remember that the ERBs are evaluated using the javascript interpreter, not coffeescript, so there’s a bit of mixing and matching of coding styles going on here.

We bind to the change event on the model so that if model is updated somewhere else, this form is automatically re rendered. We watch for the submit event on the form (by calling @delegateEvents), so that we can handle the form submission ourselves in onSubmit.

onSubmit sets some properties on the model by reading them out of the inputs, then triggers change on the model. This would normally be a call to @model.save(), but we don’t have a datastore to save the data too - so we just throw away the changes. In my own apps I use localStorage to persist the changes to the data, and then have a sync method that synchronises the local store with my production servers.

render sets the title of the page, renders the template into the .ui-content div, calls reapplyStyles so that jquery mobile can do it’s magic, and then calls delegateEvents so that the events listed in the @events hash is applied to all the created elements.

ShowVenueView

Simpler than the EditVenueView, except it generates some internal urls:

<a href="#venues-<%= venue.cid %>-edit">Edit</a>

This URL is recognized by backbone (see the routes in the controller) and will cause the edit method on the HomeController to be called. We also bind to change here as well, so that when you save changes to the form, it automatically redraws the form. This is one of the main advantages of backbone.js - you can loosely couple views and not have to think of what views need to be redrawn when you make a change to a model, it happens automatically.

HomeView

Displays the list of places. Uses the .each() method that underscore.js makes available (or delegates to the native browser implementation) to iterate over all the venues and create a link to them.

HomeController

class HomeController extends Backbone.Controller
  routes :
    "venues-:cid-edit" : "edit"
    "venues-:cid" : "show"
    "home"  : "home"

  constructor: ->
    super
    @_views = {}

  home : ->
    @_views['home'] ||= new HomeView

  show: (cid) ->
    @_views["venues-#{cid}"] ||= new ShowVenueView { model : Venues.getByCid(cid) }

  edit: (cid) ->
    @_views["venues-#{cid}-edit"] ||= new EditVenueView { model : Venues.getByCid(cid) }

I only have one controller in this app, but you could easily split it into two (a home controller and a venues controller). First up - we define the routes that we match. Note the routes have to be in an order of decreasing specificity, otherwise venues-:cid would gobble up venues-1235-edit.

I created @_views hash that I use to keep track of all the views that I have created, since I don’t want to generate two instances of the same view. This is quite a naive implementation, because views can be created dozens of times if the user clicked on every single Venue. We would either want to expire old views, or re-use views - but I haven’t worked out a way to manage that yet.

Home, show and edit are all called by the Backbone router (see Backbone.history) when the anchor part of the current url (event hashchange) changes. Each action is called with the parameters extracted from the routing hash. So all we have to do is instantiate the view and our app is ready to go.

Finally, when the document is ready we start the backbone router and render the initial view.

Conclusion

I’ve done less than a weeks development with jquery mobile (I’d done a lot more using jqTouch), but it seems like a nice complement to backbone.js. It’s fantastic that you can build an app using your current web experience, that can be sold in the Ovi Store (via Nokia Webruntime), Android Marketplace or iTunes Store (Phonegap).

I’ll try and keep things updated as I build out my own app, and see how things scale up.