Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Build a jQuery Mobile Survey App: App Logic & Interface

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

Welcome to part II of the tutorial series on creating a quiz or survey web application with jQuery Mobile and Ruby on Rails. In this part of the tutorial we are going to create a mobile friendly web interface with jQuery Mobile so that our survey can be taken easily on any HTML5 compatible smartphone. This can be done very easily as jQuery Mobile comes with pre-defined CSS templates that look great in mobile browsers in addition to being a great javascript library that helps developers create "app-like" experiences for mobile websites.

Before we start, I have included a bonus rake task inside the sample code for this part of the tutorial that generates some sample questions. To run this, simply execute this in the command line inside the directory of the Rails app:

rake setup:survey

The source for this task is located at lib/tasks/setup.rake

We'll start by generating a show action for our questions controller in app/controllers/questions_controller.rb.

def show
    @question = Question.find(params[:id])
    @choices = @question.choices
end

Our show action here is very simple. We are loading a question from the database by its ID. We are storing the choices for the question in an instance variable for later access in our view. You'll notice that since we setup a has_many relationship between the questions and choices, we "automagically" get the convenience method of being able to retrieve all the choices for a question by calling "@question.choices." By default, Rails will load our view from the show.html.erb file which we will create later.

Next, lets create the "answer" action inside our questions controller which will take a user's response to a question and store it in the database.

 def answer
    @choice = Choice.find(:first, :conditions => { :id => params[:id] })
    @answer = Answer.create(:question_id => @choice.question_id, :choice_id => @choice.id)
    
    if Question.last == @choice.question
      render :action => "thankyou"
    else
      question = Question.find(:first, :conditions => { :position => (@choice.question.position + 1) })
      redirect_to question_path(:id => question.id)
    end
 end

As we explained before when we created the table to store answers, an answer is simply the combination of a question's ID and a choice ID. Since we don't have the concept of a user in this system, we are simply going to store answers and look at the results en masse later on. Let's break it down:

    @choice = Choice.find(:first, :conditions => { :id => params[:id] })
    @answer = Answer.create(:question_id => @choice.question_id, :choice_id => @choice.id)

In the above code, we are finding the choice in the database by its id. We then are creating an answer object that is comprised of the question_id and the ID we get from the choice object.

    if Question.last == @choice.question
      render :action => "thankyou"
    else
      question = Question.find(:first, :conditions => { :position => (q.position + 1) })
      redirect_to question_path(:id => question.id)
    end

After a user answers a question, we have a decision that will determine what to show to the user. If the user has answered the last question we have stored in the database (which we can retrieve with "Question.last"), we are going to render our "thank you for completing the survey" view. If it's not the last question, we are going to find the question with a "position" of the current question's position plus 1. Then we will redirect to the show action for that question with the RESTful rails helper method of question_path. For more info on creating RESTful rails controllers, do a google search for "RESTful Rails 3" and read some of the articles that people have posted.

In the RESTful world, there is no such thing as an answer action, so we have to add this to our config/routes.rb file.

Simply replace this line:

resources :questions

with this:

resources :questions do
	collection do
	  get :answer
	end
end

Currently, if a user hits the root URL of our server, they will be given an error. To prevent this we are going to add this root option to our config/routes.rb file as well:

root :to => "questions#index"

This line will direct the request of the root URL to the index action of the question's controller. While we haven't defined an actual index action, by default Rails will load the index.html.erb file as the view. We are going to create this file later on.

Now that our Rails work is essentially done, we are going to start creating the views that will make use of the jQuery Mobile framework. We'll start by creating a global layout for our template at app/views/layouts/application.html.erb.

<!DOCTYPE html>
<html>
<head>
  <title>Survey</title>
  <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a1/jquery.mobile-1.0a1.min.css"/>
  <script src="/javascripts/jquery.min.js"></script>
  <script src="/javascripts/application.js"></script>
  <script src="http://code.jquery.com/mobile/1.0a1/jquery.mobile-1.0a1.min.js"></script>
  <%= csrf_meta_tag %>
</head>
<body>
  <div data-theme="b" class="ui-page-active" data-role="page">
    <%= yield %>
  </div>  
</body>
</html>

In the head section, you'll notice that we are loading 2 jQuery Mobile specific files from the jQuery site: 1 CSS file and 1 JS file. This is fine for development mode, but if we were to push this into production mode, we would want to bring these files into our app locally.

In the body section of our template we are creating our top level DIV that will contain all the functionality for our mobile site:

<div data-theme="b" class="ui-page-active" data-role="page">
   <%= yield %>
</div>

There are a few things to point out in this DIV. First, we are going to use one of the pre-defined jQuery Mobile themes for this site. The theme we have chosen is called "Theme B." By placing the attribute data-theme="b" in our top-level DIV, we are assigning that element to inherit the styles for Theme B. To see all of the default theme options for jQuery Mobile you can visit the following URL: http://jquerymobile.com/demos/1.0a4.1/#docs/api/themes.html

The top-level elements of all jQuery Mobile apps are called "pages." To define a page, we set the attribute data-role="page" on an element. In our app, we are going to define only one page and then load all subsequent pages via Ajax calls. However, if we had an essentially static site, we could define multiple elements of data-role="page" all at once. We could then create simple links that would navigate these "pages" and perform nice looking transitions between them. The primary page when the browser loads the site should have an "active" state. In this case, since we are only displaying a single page element, this is not that important. However, for illustrative purposes we are assigning the class "ui-page-active" to denote that this DIV is the one that should be displayed when the browser loads the site.

The next step is to create our views. We'll start with our index.html.erb view:

<div data-role="header" data-theme="b">
	<h1>Survey</h1>
</div>
<div data-role="content">
	<%= link_to "Begin Survey", question_path(Question.find(:first)), "data-role"=>"button"%>
</div>
<div data-role="footer" data-theme="b">
	<h4>Copyright 2011</h4>
</div>
Ruby Survey App

The anatomy of a jQuery Mobile page is rather simple. Each page contains 3 main sections: the header, the content, and the footer. The CSS files and javascript are designed so that with very simple HTML, you can create a dynamic, native-like experience inside a mobile website. For our header, by simply specifying the data-role="header" attribute, we have created a nice-looking header bar with a gradient background that is theme specific. We will go into more options on this later.

In our content section, we have added a standard HTML link with the Rails helper method of link_to. We have added the data-role="button" attribute to turn that ordinary link into a stylized button. The URL for the link is a path to the first question in our database as defined by the second parameter we are passing to the link_to method.

The interesting part about making sites with jQuery Mobile is that it attempts to mimic native-app behavior by default. Instead of this link redirecting our browser to brand new page, like a typical website, the jQuery Mobile library will actually convert it into an Ajax link that will pull content in from the server and display it inside a newly created "page" element. Once it is loaded, a callback function is called that will show a transition animation to the new page. By default this new page will "slide" in from the left. Again, jQuery Mobile has accomplished this goal by allowing the developer to create this native-like experience with no special markup or advanced javascript functionality.

Lastly, we will create a footer element of data-role="footer" to hug the bottom of our content section.

Next, we will create our show.html.erb view to display our survey question to the user:

<div data-role="header" data-theme="b">
  <h1>Survey</h1>
</div>
<div data-role="content">
  <div align="center">
	<h3><%= @question.question %></h3>
  </div>
  <ul data-role="listview">
    <% @choices.each_with_index do |c, i| %>
      <% i = i + 1 %>
      <li data-theme="c">
        <%= link_to "#{i}. #{c.choice}", answer_questions_path(:id => c.id) %>
      </li>
    <% end %>
  </ul>
</div>
<div data-role="footer" data-theme="b">
	<h4>Copyright 2011</h4>
</div>
Ruby Survey App

The format of this view is almost identical, as you can see. Inside our "content" element you'll notice that we have an unordered list tag with a data-role of "listview."

<ul data-role="listview">

When an unordered list is set to this data-role, it becomes a kind of navigation element with right arrows by default. This is a pretty common paradigm in mobile apps as it can be used for both nested menus as well as a kind of slideshow where each screen is a different card in a stack.

Inside our unordered list you'll notice that the list-item has a data-theme attribute specified:

<li data-theme="c">

This illustrates how jQuery Mobile's theming engine allows us to modify any element and assign a new theme to it. In this case, the mix of Theme B's parent element's but Theme C's list-item looks really good.

Inside the list element we are using the Rails helper method to create a link again which will effectively answer the question we are displaying. It is again interesting to note that we are not doing any special Javascript or Ajax calls here with this link. By default a simple anchor tag will load the URL specified in the href attribute into a new "page" element via Ajax and then display it to the user.

Lastly, we are going to create a view that has a thank you message once the user has completed the survey. This file is located here: app/views/questions/thankyou.html.erb.

<div data-role="header" data-theme="b">
  <a href="/" data-icon="home" rel="external">Home</a>
  <h1>Thank You!</h1>
</div>

<div data-role="content">
  <p data-role="content">
	Thanks for answering the survey! Have a Good Day! :)
  </p>
</div>
Ruby Survey App

This view is very similar to the others with one exception. The link inside the "header" element has a rel="external" attribute which effectively blocks jQuery Mobile from changing the standard link into an Ajax loader. Putting rel="external" inside an anchor tag will force the link to behave normally and fully redirect the browser when clicked.

You may have notice in the screenshots that when a user answers a question, they are presented with the next question immediately. By default, jQuery Mobile places a back button inside the "header" element that hugs the left side of the screen. jQuery Mobile has a sophisticated method of determining a user's path or history through the app. Hitting the back button will move the user back to a new "page" in the app that has been dynamically loaded via the Ajax call.

By placing this link inside the "header" element, it creates an interesting feature override. Links that are placed to the left of the h1 title tag will replace the back button. Since we are at the end of the survey on this screen, we don't want the user traveling backwards through the questions again. This home link will fully redirect the browser back to the starting page so that the user can answer the survey questions again.

I encourage everyone to take a look at the demos and documentation link on the jQuery Mobile website for info on the concepts covered here: jQuery Mobile Documentation

And there we have it! I hope you enjoyed this tutorial series on making a mobile web application with Ruby on Rails and jQuery Mobile.

Advertisement