jQuery Mobile & Rails
I just finished my first dive into using jQuery Mobile with a Rails application and wanted to share some techniques that came out along the way.
Hopefully these will help you if your are using jQuery Mobile with Rails or want to test your mobile application's integration layer.
This post assumes you are somewhat familiar with jQuery Mobile and its basic concepts. So let's jump right in with a series of helpful
tips.
A Mobile Layout
In my application, I decided to use use a route namespace of "mobile" vs a sub-domain for all controllers and views to reside in. Do what
works for you, but I find that using namespaces both helps keep my code organized and simple to maintain. Either way, you should have a
layout specific for your mobile application. Mine is in app/views/layout/mobile.html.haml. As you can see here, I use HAML,
so all view examples will be using it vs ERB. Here is a general layout.
!!! 5
%html{:lang => 'en'}
%head
%meta{:charset => 'utf-8'}
%meta{:name => 'viewport', :content => 'width=device-width, initial-scale=1'}
%meta{:name => 'format-detection', :content => 'telephone=no'}
%title= 'My Mobile App'
%link{:rel => 'stylesheet', :href => 'http://code.jquery.com/mobile/1.0b2/jquery.mobile-1.0b2.min.css'}
%script{:src => 'http://code.jquery.com/jquery-1.6.2.min.js'}
%script{:src => 'http://code.jquery.com/mobile/1.0b2/jquery.mobile-1.0b2.min.js'}
%body
= yield(:layout)
A few key points here. First, the meta tag for the viewport is recommended as a good base. The other meta tag for
format-detection is to disable automatic detection and linking of phone numbers. Phone number detection is way too
aggressive and often just links random numbers with periods and hyphens. This means if you want a phone number to call when touched, you
will have to use the tel: link format with the phone number afterward. I recommend the aid of something like the phony gem for validating and parsing phone number formats. Lastly, the title attribute in the head
is really moot. The jQuery Mobile framework will dynamically change the page title as new page DOM elements are loaded in.
Mobile Page IDs
A typical jQuery Mobile app will have one full page load. All other pages thereafter are loaded via AJAX and inserted into the DOM. Their docs suggest that every page (and form) have a unique id attribute. These ids can be used to link to the DOM page element when navigating around preloaded pages. Simple apps can get by with a few ids like "#home", "#contact_us", "#etc". But if you have a large application, you need a better system to keep track of things.
Thankfully if you are building your Rails applications in a RESTful manner, it is easy to leverage the route link helpers to generate
your page ids. In the example below, I created a method called mobile_page_id that I placed into a shared concerns module directory. From here I mix this into my test
helper, and application's view helper. It is that damn useful! You are going to want to use this everywhere!
# File: app/concerns/mobile_concerns.rb
module MobileConcerns
module Helpers
def mobile_page_id(path = request.path)
path.sub(/\A\/mobile\//,'').gsub("/",'_')
end
end
end
# Mix into your applications helper.
module ApplicationHelper
include MobileConcerns::Helpers
end
# Mix into your test helper of choice.
class MobileStoriesTest < ActionController::IntegrationTest
include MobileConcerns::Helpers
end
The mobile_page_id method is primarily for views. I will cover its usage in testing further down. When called without an
argument, it will take the current request path and translate it to a string suitable for a page id. So if you were rendering a
/mobile/users/10/avatar page, the id would be users_10_avatar. If your application follows RESTful resources in
your routes, this can pay dividends. When needed, you can pass a *_path helper method to get the same id. In this example
the same id would come back for mobile_page_id(mobile_user_avatar_path(@user)). Remember, I am using a "mobile" namespace in
my examples and hence my method above strips that out, flavor this helper to your needs. Here is an example of what you might find in
app/views/users/avatar.html.haml using the mobile_page_id. You can see here that I am also setting a title local that is
used in the %h1 tag and the data-title page element attribute. Doing this will show the same title in the
header bar as well as the page title.
- title = "User Profile"
%div{:id => mobile_page_id, :data => {:role => 'page', :title => title}}
%div{:data => {:role => 'header'}}
%h1= title
%div{:data => {:role => 'content'}}
edit your profile...
%div{:data => {:role => 'footer'}}
/...
Easy Data Attributes
The jQuery Mobile framework will have you typing a lot of data-* attributes into your elements. It uses these from basic page behavior to UI themes. They are literally needed everywhere. If your typing out raw HTML these data attributes get old pretty quick. Thankfully both HAML and the latest Rails 3.1 tag helpers can keep things clean. Both allow hashes to be passed to the data attribute. Keys are dasherized and values are JSON-encoded except for string and symbols.
# HAML Shorthand For:
# <div id="foo" data-role="page" data-title="Title" data-rel="dialog"></div>
%div{:id => 'foo', :data => {:role => 'page', :title => 'Title', :rel => 'dialog'}}
# Rails 3.1 Tag Helpers
# <div data-name="Stephen" data-city-state="["Chicago","IL"]" />
tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)})
Dynamically Setting Layout
Remember how jQuery Mobile only loads the entire page once and then inserts all following pages using AJAX? This means that views after
the initial page load do not need the layout when rendered. Depending on the setup of your mobile application, it could become a chore
telling each action to conditionally render the layout or not. Imagine a page refresh or a user bookmarking a deep resource node of your
mobile app. Thankfully with a little Rails-fu, we can conditionally set the layout to false if the request is an HTTP GET
from an AJAX request. Below is an example of my base mobile controller that all mobile controllers inherit from. Now every action is able
to load the entire mobile layout or not.
class Mobile::BaseController < ApplicationController
LAYOUT = 'mobile'.freeze
layout LAYOUT
around_filter :dynamically_assign_layout
private
def dynamically_assign_layout
self.class.layout false if request.get? && request.xhr?
yield
ensure
self.class.layout LAYOUT
end
end
Integration Testing & Capybara-Webkit
Those that know me are familiar that I do not use RSpec or Test::Unit but instead opt for a simple testing framework built into Ruby 1.9,
MiniTest::Spec. I also use the Capybara-WebKit driver for my acceptance testing in both Rails 2.3 and Rails
3.1's standard integration test layer. Reference the integration_test_helper.rb file in each link above to learn how to
use Capybara-WebKit with your rails app. Thanks to Wyatt Greene for his original article on the matter.
So why the fuss? Well Capybara-WebKit is a headless WebKit browser that you can direct right from your test suite. What makes it so
awesome is that it renders web pages with full JavaScript support and is much faster than selenium based Capybara drivers. Since jQuery
Mobile is entirely based on JavaScript with HTML & CSS3 - Capybara-WebKit is a perfect candidate to acceptance test your mobile
application. The only gotcha is scoping your Capybara actions to a certain page that is dynamically loaded into the DOM. No problem! This
is easily solvable using the page ids from the mobile_page_id helper method I mentioned above. So whatever your testing
framework, here are a few helpers that are critical. Assume these are mixed into your test helper.
private
include MobileConcerns::Helpers # Mentioned above.
def current_page_id(path=nil)
path = path || current_path
"div##{mobile_page_id(path)}.ui-page-active"
end
def current_page(path=nil)
find(current_page_id(path))
end
def within_current_page(path=nil)
within(current_page_id(path)) { yield }
end
So let's go over these. First is the current_page_id method. Most of the time you are going to pass a path argument to this
method, since the Capybara's current_path will only work correctly on the first page load, not each following AJAX page load
which would change the location hash. See how this is using the mobile_page_id helper mixed in and described above? Next is
the current_page helper. It finds the passed path/id with Capybara's find method. The within_current_page
leverages Capybara's within helper to scope your action to that particular DOM element. Here is a classic example using
these.
should 'be able to navigate to logged in user page and change email' do
login_as @user
visit mobile_homepage_path
click_on 'My Account'
user_path = mobile_user_path(@user)
assert current_page(user_path)
# Change email
new_email = Forgery(:email).address
within_current_page(user_path) do
fill_in 'email', :with => new_email
click_button 'Save Changes'
end
@user.reload.email.must_equal new_email
end
Hopefully you can see how this simple page id foundation can help you better test your jQuery Mobile app with Capybara-WebKit. What an awesome tool! I sometimes find it hard to believe we are now at the point where we can easily test this much JavaScript in a headless browser directed by Ruby.