21 May 2012
Intermediate
The mobile web is huge and it's continuing to grow at an impressive rate. Along with the massive growth of the mobile internet comes a striking diversity of devices and browsers. It's not all WebKit, it's definitely not all iOS, and even when it is WebKit there are a vast array of implementation differences between browser APIs. As a result, making your applications cross-platform and mobile-ready is both important and challenging.
jQuery Mobile provides a way to jumpstart development of mobile-friendly applications with semantic markup and the familiarity of jQuery. Rails provides an easy-to-use application environment for serving that markup and managing the data that backs it. By and large they work together flawlessly to create extraordinary mobile experiences, but there are a few integration points that bear highlighting.
In this article, I highlight and then smooth over the rough edges of the integration between these two frameworks. You'll need some basic knowledge of how jQuery Mobile works including an understanding of what data-* attributes are and how they are used to create jQuery Mobile pages, headers, and content blocks. For a quick introduction to the basics there's a great article by Shaun Dunne at ubelly.com that covers everything necessary for this article. Additionally you should be familiar with the basics of building applications in Rails, including form validation flow, templating, layouts, and the asset pipeline.
All the examples and advice in this article are derived from the construction of a sample application that tracks the presence of employees in an office. To access the code for this application, download the sample files for this article or visit https://github.com/johnbender/jqm-rails. It is admittedly a simple application, and complexity often shines a light in little dark corners so if you have suggestions please add them in the comments. Otherwise, the README has detailed setup instructions for the application if you want to play around with it.
All jQuery mobile applications expect a certain set of includes in a particular order. When integrating with Rails the default recommendations made in the jQuery Mobile documentation require some slight alterations. The recommended configuration looks something like the following:
<link rel="stylesheet" href="$CDN/jquery.mobile.css"/>
<script src="$CDN/jquery-1.6.4.min.js"></script>
<script src="$CDN/jquery.mobile.js"></script>
Where $CDN is either your own content delivery network or //code.jquery.com/mobile/$VERSION/ . Rails, as of version 3.1, uses the jquery-rails gem by default in a newly generated application's Gemfile and includes it via the asset pipeline. So your includes will actually take the following form:
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
<script src="$CDN/jquery.mobile.css"></script>
<script src="$CDN/jquery.mobile.js"></script>
Since the jQuery JavaScript is rolled into the application, include it through the asset pipeline with the following directive:
//= require jquery
At the time of this writing there is one issue with jQuery Mobile 1.1 and jQuery Core 1.7.2— a newly generated Rails Gemfile doesn't have a constraint on the jquery-rails gem version. So in your Gemfile it's a good idea to use gem 'jquery-rails', '=2.0.1' , which carries jQuery Core version 1.7.1 and is compatible with jQuery Mobile 1.1. After that the only thing left is to decide what you want to do with your viewport meta tag. The discussion about device scale and width is a long and complex one. For more details see Peter-Paul Koch's post titled A pixel is not a pixel is not a pixel. The recommended tag for jQuery Mobile applications is:
<meta name="viewport" content="width=device-width, initial-scale=1">
When building your jQuery Mobile application the navigable views are constructed using data-role=page annotated div elements. Fortunately Rails provides a variety of ways to get your content and visual information into that div , but you'll have to decide which fits your use case best.
The first and least complicated involves simply rendering all your view content into a data-role=page div element in the top level application layout (see app/views/layout/application.html.erb in the sample files):
<body>
<div data-role="page">
<div data-role="header">
<h1><%= yield :heading %></h1>
</div>
<div data-role="content">
<%= yield %>
</div>
</div>
</body>
Here the main content of a view will be rendered into the yield call and a content_for block can be used to push bits of content elsewhere. A simplified version of the users index at app/views/users/index.html.erb view would look something like the following:
<% content_for :heading do %>
All users
<% end %>
<ul data-role="listview" data-filter="true">
<% @users.each do |user| %>
<li><%= user.email %> - <%= user.status %></li>
<% end %>
</ul>
This at least reduces the burden on the view itself leaving a large chunk of the work in keeping the pages consistent to the layout. As the complexity of your Rails application grows it's likely that the views will need to make more detailed alterations to their parent layouts that don't make sense as content_for yields. One approach is to create a jQuery Mobile page partial and use a render block (see app/views/shared/_page.html.erb); for example:
<div data-role="page">
<div data-role="header">
<h1><%= h1 %></h1>
</div>
<div data-role="content">
<%= yield %>
</div>
</div>
Using it in a view would take the following form (see app/views/shared/sample.html.erb):
<% render :layout => 'shared/page',
:locals => { :h1 => "foo" } do %>
<div>The Content</div>
<% end %>
This has the advantage of pushing the control down into the views a bit more and making the page configuration requirements more explicit by requiring the user to provide the :locals values. As you'll see, being able to tightly control the configuration of the page elements with data attribute values is important.
jQuery Mobile's support for caching multiple pages in an HTML document can cause issues for Rails form validation, as well as any sequence of actions that result in navigating to the same URL many times in a row. By default, the pages that exist in the HTML document will be removed when navigating away from them but in general the framework tries to source content and views locally where possible. The following simple example illustrates this scenario:
<div data-role="page" data-url="/foos">
<div data-role="content">
<a href="/bars">Go to Bars</a>
All the Foos
</div>
</div>
<div data-role="page" data-url="/bars">
<div data-role="content">
<a href="/bars">Go to Bars</a>
All the Bars
</div>
</div>
Assuming the first page is the current active page and DOM caching is turned on, clicking one of the /bars links will navigate to the page that already exists in the DOM from that URL (the data-url is added by the framework to identify from where the content came). As a consequence, clicking the /bars link on the /bars page is effectively a no-op. This is important in Rails because invalid form submissions render the new view consistently under the index path (see app/controllers/users_controller.rb).
def create
@user = User.new(params[:user])
if @user.save
redirect_to root_url
else
# /users == /users/new
render :new
end
end
On validation failure, the content of /users is effectively identical to /users/new , save for the possible addition of the error message markup. The problem is that the page content for /users also has a form that submits to /users as its action, which is the aforementioned no-op.
The solution I normally recommend is to add data-ajax=false on the form, which will prevent the framework from hijacking the submit. Unfortunately that also means it won't pull the content and apply an animation/transition. One quick way to get around the problem and retain the nice transitions is to differentiate the action path using a URL parameter with a helper (see app/helpers/application_helper.rb).
# NOTE severely pushing the "clever" envelope here
def differentiate_path(path, *args)
attempt = request.parameters["attempt"].to_i + 1
args.unshift(path).push(:attempt => attempt)
send(*args)
end
As noted in the comment, this is probably a bit too clever (pejorative form), but it handles differentiating parameterized or unparameterized Rails paths and URL helpers by adding an attempt query parameter. In use as the :url hash parameter to the form_for and form_tag helpers it looks like the following:
# new form
:url => differentiate_path(:users_path)
# edit form
:url => differentiate_path(:user_path, @user)
For each new submission it will increment the parameter value and signal to jQuery Mobile that the path and the content are different. In addition you will want to annotate your form page with data-dom-cache=true so that it preserves the previous form submission page contents for a sane back button experience (easier with the _page partial). Otherwise jQuery Mobile will reap the previous form validation failure pages from the DOM and try to reload the requested URL in the history stack. If that happens to be /users?attempt=3 the content won't be the submission form but rather a list of the users or something else if that URL requires validation. By preserving the pages, the back button will simply let users traverse backwards through their submission failures.
jQuery Mobile makes heavy use of data attributes for annotating DOM elements and configuring how the library will operate. During beta we came to the consensus that data attribute use was becoming more and more common and decided that a namespacing option would be valuable. Rails also makes fairly heavy use of data attributes for its unobtrusive JavaScript helpers, though it doesn't appear from a simple grep data- jquery_ujs.js that there are any conflicts. If that changes you can alter jQuery Mobile's data attribute namespace with a simple addition to app/assets/javascripts/application.html.erb:
//= require jquery
//= require jquery_ujs
//= require .
$( document ).on( "mobileinit", function() {
$.mobile.ns = "jqm-";
});
The mobileinit event fires before jQuery Mobile has enhanced the DOM and is generally where you configure the framework with JavaScript. As a result it's important that the binding comes after the inclusion of jQuery in the asset pipeline and before jQuery Mobile is included either in the pipeline or in the head of your document. With the above snippet in place the data attributes in the page partial would change to the following:
<div data-jqm-role="page">
<div data-jqm-role="header">
<h1><%= h1 %></h1>
</div>
<div data-jqm-role="content">
<%= yield %>
</div>
</div>
If you are beginning a new application and you plan to use libraries that rely on data attributes it might be better to start out with a namespace since changing it after the fact can be time consuming and error prone.
Tooling for mobile web development is still evolving, and though Weinre and Adobe Shadow present intersecting opportunities to debug CSS, markup, and JavaScript you can still expect server-side errors. jQuery Mobile, being unaware of the environment in which it's working, must report a server error in a user friendly fashion. As a result it swallows the Rails stack traces you've come to know and love and just displays an error alert. By binding the special pageloadfailed event you can replace the DOM content with the stack trace when one occurs (see app/assets/javascripts/debug/pagefailed.js.erb).
function onLoadFailed( event, data ) {
var text = data.xhr.responseText,
newHtml = text.split( /<\/?html[^>]*>/gmi )[1];
$( "html" ).html( newHtml );
}
$( document ).on( "pageloadfailed", onLoadFailed);
To make sure that it only loads in development you can wrap that in a <%= if Rails.env.development? %> block and the asset pipeline will render the erb without the snippet in production or test environments.
Note: I'd like to thank some helpful attendees at my RailsConf talk who informed me about using erb in the asset pipeline! If that's you please contact me on twitter or GitHub.
If you're interested in taking this a bit further, jQuery defines its constituent modules using Asynchronous Module Definition (AMD), so integrating require.js into the asset pipeline and defining a meta module for just the parts you want is one way to reduce the wire weight of the include. Also it's worth examining WURFL integration through the gem of the same name if you are creating a mobile version of an existing website and you want to redirect users properly. Otherwise, Rails and jQuery Mobile form an exceptionally productive combination for building mobile web applications.
If you find errors in the sample application please fork the sample application repository, make the alteration to doc/post.md, and submit a pull request.