Customizing Rails Applications
on Mac OS X Leopard

In Developing Rails Applications on Mac OS X Leopard we created a web application using the latest Rails features with Xcode 3.0. In this article, we'll go into customizing the views, working with web forms, and adding AJAX and iPhone support.

This is the second in a series of three articles:

  • Development, where you learn to build a basic RESTful Rails application using Xcode 3.0;
  • Customization, this article, where we discuss working with views and web forms, adding AJAX support, and supporting an iPhone interface;
  • Deployment, where we set up version control, write a Capistrano recipe, and deploy on Leopard Server.


Together they will give you a great start in working with Rails on Mac OS X Leopard.

Customizing Views

Previously, we added expenses to an event in script/console and implemented business logic to calculate the total expenses for the event. However, we don't see an event's expenses when we show an event in the browser. That's because we're still using the scaffold-generated show template to display the event's details. Scaffolding is a good start, but it's meant to be customized.

Partial Templates

To list an event's expenses at the bottom of the event's show template, we're going to use a partial template. Partials are a great way to decompose view templates for better maintainability and reuse. In fact, we'll use this partial template again a bit later when we sprinkle in some AJAX.

Create the partial template by clicking on the app/views/events folder in the Organizer, right-mouse click, and choose "New File". Name the partial template file _expenses.erb (note that partial files always begin with an underscore). Then paste the following into the new file:

<table>
<% for expense in expenses -%>
  <tr id="<%= dom_id(expense) %>">
    <td>
      <%= link_to expense.vendor.name, expense.vendor %>
    </td>
    <td align="right">
      <%= number_to_currency(expense.amount) %>
    </td>
  </tr>
<% end -%>
  <tr>
    <td align="right" colspan="2">
      <strong>Total</strong>:
      <%= number_to_currency(event.total_expenses) %>
    </td>
  </tr>
</table>

The partial's job is fairly easy: It loops through all the expenses in the expenses array, generating an HTML table row for each expense. Notice the use of the built-in dom_id helper to generate a unique HTML element id for each expense row. We've also used the built-in number_to_currency view helper to display the expense amount as dollars.

Next, call the partial at the bottom of the event's show template by adding the following to the end of the app/views/events/show.html.erb file:

<h3>Itemized Expenses</h3>

<div id="expenses">
  <%= render :partial => 'expenses',
             :locals  => {:expenses => @event.expenses,
                          :event    => @event} %>
</div>

Calling a partial template is similar to calling a method or subroutine: we give the name of the partial template and any data the partial needs to do its work. The name is expenses, which by convention corresponds to the app/views/events/_expenses.erb partial template file. The partial needs an event's expenses and the event itself, which we assign to the local variables expenses and event respectively.

View Helpers

At this point, we have everything in place to list an event's expenses. Now let's add a little icing. The last row of the _expenses.erb partial template shows the total of all expenses. When this row is generated, we want to change its style based on the expense total. If the expense total is greater than the event budget, then the total should be displayed in red. Otherwise, the total should be displayed in black. (The club treasurer likes it better this way.)

We could put this view-specific logic directly in the _expenses.erb partial template, but there's a better way. Instead, we'll write a helper method and call it from the partial template.

Update the app/helpers/events_helper.rb file as follows:

module EventsHelper
  def expense_total_style(event)
    event.budget_exceeded? ? 'color: red' : 'color: black'
  end
end

This helper method simply calls the budget_exceeded? method we previously added to the Event model. If it returns true, the CSS style for red text is returned. Otherwise, the CSS style for black text is returned. It's a trivial amount of code that we could have slapped right into the template, but making it into a helper means we can reuse it in other views that show expense totals.

Then, in the app/views/events/_expenses.erb partial template file, change the last row of the table to call the expense_total_style view helper method:

<strong>Total</strong>:
<span id="total" style="<%= expense_total_style(event) %>">
  <%= number_to_currency(event.total_expenses) %>
</span>

Time for some gratification! Navigate to the show page for one of the events you added previously using script/console. If you added enough expenses to bust the budget, you should see something similar to the page shown in Figure 12.

Event Expenses

Figure 12: Customized view using a partial template and a view helper

Working with Web Forms

We don't yet have a way to record expenditures for an event using the web interface. We need a good ol' web form to enter expenses. Again, the scaffold-generated code is a good place to learn how to get started.

Basic Forms

Forms for creating and updating a single resource such as an event are fairly straightforward. For example, in the app/views/events/new.html.erb file you'll see:

<%= error_messages_for :event %>

<% form_for(@event) do |f| %>
  <p>
    <b>Name</b><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <b>Budget</b><br />
    <%= f.text_field :budget %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

This template generates an HTML form using built-in form helpers. The form_for helper generates an HTML form tag for an event. The text_field helper generates the HTML input tag bound to the corresponding attributes of the event. (Rails includes form helpers for all the standard HTML form elements.) Finally, the submit helper generates a submit button.

In this case, because the new template is used to create new events, the form posts its data to the create action of the EventsController in app/controllers/events_controller.rb:

def create
  @event = Event.new(params[:event])

  respond_to do |format|
    if @event.save
      flash[:notice] = 'Event was successfully created.'
      format.html { redirect_to @event }
      ...
    else
      format.html { render :action => "new" }
      ...
    end
  end
end

Inside the create action, all the form data for the event is available in params[:event]. With this data in hand, the create action simply instantiates a new Event model object with the form data and attempts to save the event to the database. If the event isn't valid, the save operation will fail and the new action's template is re-rendered to show the form with errors. If the event is valid, it's saved in the database and we get redirected to the event's show page.

Nested Resource Forms

Now let's use what we've learned to build a new form to record expenses for an event. It's always good to start with a goal in mind, then write the code that gets you there. We want the form shown in Figure 13 to appear at the bottom of the event's show page.

Leopard Web Form

Figure 13: Recording expenses through a web form

For that to work, the form needs some data: an empty Expense object and the names of all the vendors in our database. Setting up data for a view template is a controller action's job, so we'll start there.

Update the show action of the EventsController in app/controllers/events_controller.rb to create @expense and @vendors instance variables as follows:

def show
  @event = Event.find(params[:id])

  @expense = Expense.new
  @vendors = Vendor.find(:all, :order => 'name')

  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :xml => @event }
  end
end

Next we need to think a little about how we'll interact with expenses. Every expense is associated with an event. Whenever we do anything with an expense, it's in the context of an event. We can represent this event/expenses nesting in the URLs using a nested route.

Update the config/routes.rb file to nest expenses within events as follows:

map.resources :events, :has_many => :expenses
map.resources :vendors

Then add the following form to the bottom of the app/views/events/show.html.erb file:

<h3>Add an expense to this event</h3>

<p style="color: red"><%= flash[:error] %></p>

<% form_for [@event, @expense] do |f| -%>
  <p>
    <%= f.collection_select :vendor_id, @vendors, :id, :name %>
    in the amount of $<%= f.text_field :amount, :size => 9 %>
  </p>
  <%= f.submit 'Add this expense' %>
<% end -%>

Notice that we're using form_for to generate a form for two resources: event and expense. Remember, we always work with expenses in the context of their event, so the form needs both objects. The generated HTML form tag looks like this:

<form action="/events/1/expenses" method="post">

According to the RESTful routing rules we have in place, this form will post to the create action of the ExpensesController. Add the following create action to the ExpensesController in app/controllers/expenses_controller.rb:

def create
  @event = Event.find(params[:event_id])
  @expense = @event.expenses.build(params[:expense])

  respond_to do |format|
    if @expense.save
      flash[:notice] = 'Expense was successfully created.'
      format.html { redirect_to @event }
    else
      flash[:error] = @expense.errors.full_messages.to_sentence
      format.html { redirect_to @event }
    end
  end
end

Inside the create action, the event id is available in params[:event_id] and the expense form data is available in params[:expense]. After finding the existing event in the database, we use build to populate a new Expense model object from the posted form data and add the expense to the event's collection of expenses. Then we redirect the browser to the event's show page to display the event and all its expenses.

Go ahead and use the web form on one of your event's show page to add expenses to the event. You can get insight into what's going on behind the scenes (including the SQL that's being run) by watching the log/development.log file. It's tailed automatically in the Debugger window that opened when you started the application in the Organizer.

Spicing It up with AJAX

Recording a new expense currently forces a reload of the entire show page. That is, when we submit the form it sends a synchronous request back to the server which adds the expense, issues a full redirect, and re-renders the show template back to our browser. It works, but we can improve the user experience with a bit of AJAX.

When you create a new Rails application, the Prototype and Script.aculo.us JavaScript libraries come along for the ride. You'll find them in the public/javascripts directory. Better yet, Rails includes helpers that let you tap into the power of these libraries at a fairly high level.

Again, we'll start with a goal. When a new expense is added to an event, we want to asychronously update the event's show page to

  1. Add the expense to the list of itemized expenses
  2. Update the expense total
  3. Highlight the new expense and the updated total

Start by adding the following to the <head> section of the app/views/layouts/events.html.erb file to load all the JavaScript libraries:

<%= javascript_include_tag :defaults %>

Then update app/views/events/show.html.erb to replace form_for with remote_form_for, like so:

<% remote_form_for [@event, @expense] do |f| -%>

Reload the page and view the source, and you'll see the Prototype code that remote_form_for generates. If you were to submit the form, the browser would expect JavaScript code to be returned. So in the create action of the ExpensesController we need to update the respond_to block to respond appropriately. Here's the updated create action in app/controllers/expenses_controller.rb:

def create
  @event = Event.find(params[:event_id])
  @expense = @event.expenses.build(params[:expense])

  respond_to do |format|
    if @expense.save
      flash[:notice] = 'Expense was successfully created.'
      format.html { redirect_to @event }
      format.js # renders create.js.rjs
    else
      format.html { redirect_to @event }
      format.js do
        render :update do |page|
          page.redirect_to @event
        end
      end
    end
  end
end

If the expense is successfully created, the action will render the create.js.rjs template. Otherwise, it redirects back to the event's show page.

Create the app/views/expenses/create.js.rjs file and add the following:

page[:expenses].replace_html :partial => 'events/expenses',
                             :locals  => {:expenses => @event.expenses,
                                          :event    => @event}
page[@expense].highlight
page[:total].highlight

This RJS template is Ruby code that generates JavaScript. The page object is the real workhorse. First, it generates JavaScript (Prototype code) to update the contents of the HTML element on our show page that has the id expenses. Notice that we're reusing the app/views/events/_expenses.erb partial template we created earlier to generate the table of expenses. Then the page object generates JavaScript (Script.aculo.us code) to perform highlighting effects on the table row that contains the newly-added expense and the total.

All this happens behind the scenes. If you add a new expense, you won't see Safari's address bar update. Instead, the JavaScript returned from the server is evaluated in the browser to update the page in place.

Now let's quickly repeat the cycle to delete expenses using AJAX, this time using a hyperlink.

In the app/views/events/_expenses.erb template, add a column to each expense row that includes a link to delete the expense, like this

<%= link_to_remote 'delete',
      :url => event_expense_url(event, expense),
      :confirm => 'Are you sure?',
      :method => :delete %>

The link_to_remote helper generates JavaScript to send an asynchronous request when the hyperlink is clicked. According to the RESTful routing rules and use of the delete method, the request will be routed to the destroy action of the ExpensesController. Add the following destroy action to the app/controllers/expenses_controller.rb file:

def destroy
  @event   = Event.find(params[:event_id])
  @expense = @event.expenses.find(params[:id])

  @expense.destroy

  respond_to do |format|
    flash[:notice] = 'Expense was successfully deleted.'
    format.html { redirect_to @event }
    format.js # renders destroy.js.rjs
  end
end

The RJS template is similar to the one we created earlier. The only difference is it doesn't need to highlight an expense, just the updated total. Add the following RJS code to the app/views/expenses/destroy.js.rjs file:

page[:expenses].replace_html :partial => 'events/expenses',
                             :locals  => {:expenses => @event.expenses,
                                          :event    => @event}
page[:total].highlight

Now when you click the link to delete an expense, it'll happen in the background. You'll know it worked when the list of expenses is dynamically updated and the total is highlighted.

AJAX can be tricky to debug, so do yourself a favor by getting your application working without AJAX first. Then sprinkle in AJAX when and where it benefits the user.

Integrating with Other Systems

Let's now imagine we'd like to introduce this application to another, and let them speak XML to each other. For example, suppose we have an existing accounting system that will actually pay the event expenses. To do that, it needs to talk to our new application to get the expenses.

Here's where things get interesting. The RESTful conventions give us a common lingo for accessing our resources. And the respond_to block gives us a way to vary how those resources are represented. So without any changes to our application, the accounting system (the client program) can fetch all the events from our expenses application simply by tacking .xml to the end of the URL:

http://localhost:3000/events.xml

The response is an XML document representing the events. Here's an example:

<?xml version="1.0" encoding="UTF-8"?>
<events type="array">
  <event>
    <budget type="decimal">150.0</budget>
    <id type="integer">1</id>
    <name>Chili Cookoff</name>
  </event>
  <event>
    <budget type="decimal">25.0</budget>
    <id type="integer">2</id>
    <name>Car Wash</name>
  </event>
</events>

This works out of the box for two reasons: 1) Active Record models can be serialized as XML and 2) the relevant actions of the scaffold-generated EventsController include format.xml stanzas to respond to requests for XML. Here's what the index action does, for example:

format.xml { render :xml => @events }

Of course the accounting system needs the expenses for each event, too. To do that, add the following index action to the ExpensesController in app/controllers/expenses_controller.rb:

def index
  @event = Event.find(params[:event_id])
  @expenses = @event.expenses

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @expenses }
  end
end

We can then get an XML representation of the expenses for an event using a URL that includes the event's identifier, for example:

http://localhost:3000/events/1/expenses.xml

The response is another XML document. Here's an example:

<?xml version="1.0" encoding="UTF-8"?>
<expenses type="array">
  <expense>
    <amount type="decimal">75.0</amount>
    <event-id type="integer">1</event-id>
    <id type="integer">1</id>
    <vendor-id type="integer">1</vendor-id>
  </expense>
  <expense>
    <amount type="decimal">25.0</amount>
    <event-id type="integer">1</event-id>
    <id type="integer">2</id>
    <vendor-id type="integer">2</vendor-id>
  </expense>
  <expense>
    <amount type="decimal">65.0</amount>
    <event-id type="integer">1</event-id>
    <id type="integer">3</id>
    <vendor-id type="integer">3</vendor-id>
  </expense>
</expenses>

The accounting system (or any other system) might also want to create, update, or delete events and expenses. Doing that would require sending a hunk of XML to the appropriate resource with the corresponding HTTP verb. However, working with raw XML isn't necessarily convenient. Instead, we'd like a Ruby program that handles all that for us. Enter Active Resource.

An Active Resource client is a standalone Ruby program. It doesn't need Rails, just the Active Resource gem that comes with Rails. Here's an example program that prints all the event names and updates the budget of one event:

require 'rubygems'
require 'activeresource'

class Event < ActiveResource::Base
 self.site = "http://localhost:3000"
end

events = Event.find(:all)
puts events.map(&:name)

e = Event.find(1)
e.budget = 160.00
e.save

Active Resource is like Active Record, but for resources—it uses RESTful conventions to access Rails resources. All the standard CRUD-level operations are available, as if the Event proxy class was a real Active Record model.

Put this code in a file called event_client.rb, for example, and then just run it by typing

$ ruby event_client.rb

Supporting an iPhone Interface

We've used the respond_to block to handle requests for HTML, XML, and JavaScript. These are built-in MIME types. We can take this a step further by adding new MIME types and then using the format in the respond_to block.

For example, suppose we want to support an optimized HTML interface of our expenses application for Mobile Safari browsers: the iPhone and iPod Touch. Let's give it a try!

Start by adding the following line in the config/initializers/mime_types.rb file:

Mime::Type.register_alias "text/html", :mobilesafari

This defines the mobilesafari format based on the existing text/html MIME type.

Next, update your app/controllers/application.rb file as follows:

class ApplicationController < ActionController::Base
  helper :all
  protect_from_forgery

  before_filter :adjust_format_for_mobilesafari

private

  def adjust_format_for_mobilesafari
    if request.env["HTTP_USER_AGENT"] &&
       request.env["HTTP_USER_AGENT"][/(Mobile\/.+Safari)/]
      request.format = :mobilesafari
    end
  end
end

This code uses a Rails before filter to check the user agent of each incoming request to see if it's a Mobile Safari device calling. If so, we set the request format to the :mobilesafari format.

Then update the index action of the EventsController in app/controllers/events_controller.rb as follows to support listing all the events on Mobile Safari devices:

def index
  @events = Event.find(:all)

  respond_to do |format|
    format.html   # renders index.html.erb
    format.xml  { render :xml => @events }
    format.mobilesafari # renders index.mobilesafari.erb
  end
end

Finally, create a app/views/events/index.mobilesafari.erb template file which generates a Mobile Safari-specific response. You can test that the mobilesafari format is being properly handled by pointing Safari at

http://localhost:3000/events.mobilesafari

You'll need to add a format.mobilesafari line to the respond_to block of each action in your application, and write Mobile Safari-specific template files that follow the mobilesafari.erb naming convention. Redesigning the user interface of the expenses application for Mobile Safari devices is beyond the scope of this article. Be sure to review the iPhone Human Interface Design Guidelines for Web Applications.

Now check out the third article in this series, Deploying Rails Applications on Leopard, where we finish up by deploying this Rails application on Mac OS X Leopard Server.

Updated: 2008-09-29