RJS in Practice: The Expense Tracker

So far we have completed the “Thought Log” application and taken a look at how RJS fits into the Rails framework. Now it is time to examine an example that is a bit more realistic and solves some of the problems that you might actually run into in your own projects. My expenses have been getting out of hand lately, so let's build a simple application to help track them.

Creating the Models

First, we'll run the Rails model generator to create the models used throughout this project. The Rails model generator automatically creates stub files for the models and database migrations. Then we'll edit the generated files to add our own functionality.

expenses> ruby script/generate model Project
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/project.rb
      create  test/unit/project_test.rb
      create  test/fixtures/projects.yml
      create  db/migrate
      create  db/migrate/001_create_projects.rb

expenses> ruby script/generate model Expense
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/expense.rb
      create  test/unit/expense_test.rb
      create  test/fixtures/expenses.yml
      exists  db/migrate
      create  db/migrate/002_create_expenses.rb

The generator creates the Project model in app/models/project.rb and the Expense model in app/models/expense.rb, along with unit test stubs and test fixtures. The generator also created two migrations for us: db/migrate/001_create_projects.rb and db/migrate/002_create_expenses.rb.

Now that the model generator has created these two new migrations, we need to add the column definitions that will be used by the models as attributes. For now we'll only track the name of each project. Open up db/migrate/001_create_projects.rb and edit it to look like this:

class CreateProjects < ActiveRecord::Migration
  def self.up
    create_table :projects do |t|
      t.column :name, :string
    end
  end

  def self.down
    drop_table :projects
  end
end

We only added a single line, t.column :name, :string, to the migration. This line adds the column name of type String to the database table projects. Next, define the columns for the expenses table. Same routine: open up db/migrate/002_create_expenses.rb and add the columns project_id, description and amount.

class CreateExpenses < ActiveRecord::Migration
  def self.up
    create_table :expenses do |t|
      t.column :project_id, :integer
      t.column :description, :string
      t.column :amount, :float
    end
  end

  def self.down
    drop_table :expenses
  end
end

Assuming that the database has already been created and the database connection has been configured, we can run the migrations. This will add the tables and columns defined in the two migration files to the development database configured in config/database.yml.

expenses> rake migrate

Now that the database contains the schema for the Expense Tracker we can define the relationships between the models. A Project has many Expense objects, so add the has_many() relationship to the Project model in the file app/models/project.rb.

class Project < ActiveRecord::Base
  has_many :expenses, :dependent => :delete_all
end

We added the :dependent => :delete_all option to the has_many() call because we don't want any orphaned expenses lingering around in our database without a Project. Now define the belongs_to() relationship in the Expense model. An Expense object belongs_to() a Project because the Expense contains the foreign key. Open up app/models/expense.rb.

class Expense < ActiveRecord::Base
  belongs_to :project
end

Now that the models are defined and the database is ready to go we can move on to the next step─generating and defining the controllers.

Defining the Controllers

Let's generate two controllers. The first controller is for projects and the second is for expenses.

expenses> ruby script/generate controller Projects
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/projects
      exists  test/functional/
      create  app/controllers/projects_controller.rb
      create  test/functional/projects_controller_test.rb
      create  app/helpers/projects_helper.rb

expenses> ruby script/generate controller Expenses
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/expenses
      exists  test/functional/
      create  app/controllers/expenses_controller.rb
      create  test/functional/expenses_controller_test.rb
      create  app/helpers/expenses_helper.rb

Once again, using the generator simplified the process of adding new functionality to the project. The controller generator not only automatically creates the controller file requested, but also creates the view folder and functional test stub for the controller.

The show() action simply finds the Project object from the value associated with the key :id in the params Hash. Edit app/controllers/projects_controller.rb and add the following code to the skeleton that the generator created for us.

class ProjectsController < ApplicationController
  def show
    @project = Project.find(params[:id])
  end
end

Next, edit app/controllers/expenses_controller.rb and add the code to create a new Expense object.

class ExpensesController < ApplicationController
  before_filter :find_project

  def new
    @expense = @project.expenses.create(params[:expense])
  end

  private
  def find_project
    @project = Project.find(params[:project])
  end
end

The ExpensesController is a bit more complex than the ProjectsController. Since every Expense object belongs to a Project we can save a lot of effort and duplicate code by using a before_filter. The before_filter executes before each controller action. The filter we've defined automatically finds the Project based on the value associated with the :project key in the params Hash and stores it in the instance variable @project.

The new action, as the name indicates, adds a new Expense object to a Project. Rails follows the same convention for RJS templates as for RHTML and RXML templates. Rails looks for a template with the same name as the controller action with the corresponding file extension. So in the case of the new action, the controller automatically discovers the view app/views/expenses/new.rjs.

Setting Up a Route

One more step is needed to wire up the before_filter and create nice-looking URLs. The controller looks up the project using the value stored in params[:project]. We need to add a simple route to catch this and produce nice URL paths like /projects/1/expenses/new.

Open up config/routes.rb and add the following line above the default route at the bottom of the file:

map.expenses 'projects/:project/expenses/:action/:id', :controller => 'expenses'

The change to the routing should be picked up right away if you're running your application in development mode. Moving right along, we're going to create a layout for all of our application templates.

Creating an Application Layout

We need a layout to put our content in. ActionController::Base descendants automatically look for a layout based on the name of the controller's class name, with Controller removed. This means that ApplicationController will automatically use a layout named application.rhtml. Our controllers are all descendants of ApplicationController, so the layout will inherit as well, unless overridden. Create app/views/layouts/application.rhtml and add the following code with your favorite text editor:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <title>Expense Tracker</title>
    <%= stylesheet_link_tag "screen.css" %>
    <%= javascript_include_tag :defaults %>
  </head>
  <body>
    <div id="content">
      <%= yield %>
    </div>
  </body>
</html>

Notice that we are also using the Rails helper stylesheet_link_tag() to include the stylesheet screen.css, which is found in public/stylesheets/screen.css. The stylesheet I'm using is very basic and looks like the following:

th { text-align: left; }
#content { margin: 10px; }
#content p { margin-bottom: 10px; }
#expenses,#summary { border: none; border-collapse: collapse; width: 600px;}
.amount { width: 40%; }
.amount, .total { text-align: right; }
#new-expense { margin-top: 2em; background-color: #eeede5; padding: 1em; }
#new-expense h3 { margin-top: 0.5em; }
#total { border: none; border-collapse: collapse; width: 600px;}
#total-amount, .total { font-weight: bold; background-color: #eeede5; }
#total-amount { border-top: 2px solid black; }
.total { width: 90%; padding-right: 10px; }

Just because my stylesheet is basic and boring doesn't mean that yours has to be. Dress up your Expense Tracker to your heart's content.

Entering Some Data

We need some data to display on our page, so I'll enter a few expenses that I incurred while writing this document. Start up the console.

expenses> ruby script/console
>> rjs_book = Project.create(:name => 'RJS Templates for Rails')
=> #<Project:0xb72e444c8 ...>
>> rjs_book.expenses.create(:description => 'Americano at Bridgehead', :amount => 1.93)
=> #<Expense:0xb72cb84c ...>
>> rjs_book.expenses.create(:description => 'Sandwich at La Bottega', :amount => 4.27)
=> #<Expense:0xb72c3b24 ...>
>> quit

I don't think I'll get away with writing off expenses like those, but at least I'll be able to get a better idea about where my money is being spent. Now we need some views to present this sample data.

Creating the Views

There isn't yet a way for us to view our expenses for this Project. We need to create a view that shows the Expense objects for the sample Project that was just created. The views are separated into one template and two partial templates. We specifically separate out the _expense.rhtml partial so that we can render an individual row when updating the table with RJS.

Create the view app/views/projects/show.rhtml and add the following:

<h1><%= @project.name %></h1>

<h2>Expenses</h2>
<table id="expenses">
  <tr><th>Description</th><th class="amount">Amount</th></tr>
  <%= render :partial => 'expenses/expense', :collection => @project.expenses %>
</table>

<%= render :partial => 'expenses/new' %>

This partial gives the table the id expenses so that we can refer to it when updating the page. We've also given a relative path to the partial, because we're rendering show.rhtml from ProjectsController, but we're keeping the expense partials in the app/views/expenses view folder.

Now we create partial app/views/expenses/_expense.rhtml. This partial renders the actual Expense within the <table>.

<tr id="expense-<%= expense.id %>">
  <td><%=h expense.description %></td>
  <td class="amount"><%=h number_with_precision(expense.amount, 2) %></td>
</tr>

Notice how each row is given an id based on the Expense object's id attribute. We've done this for the same reason that we gave the <table> an id: it allows us to refer to the row in the future. The method number_with_precision() is just a built-in Rails helper method that displays the amount with the specified number of decimal places. We also used the h() method, which escapes the HTML rendered on the page. Escaping the HTML prevents a malicious user from adding JavaScript scripts to the project's title.

Last, but not least, we'll add the partial for the form. Place the following code into app/views/expenses/_new.rhtml:

<div id="new-expense">
  <h3>Add an expense</h3>
  <% form_remote_for :expense,
                     Expense.new,
                     :url => hash_for_expenses_url(:project => @project,
                                                   :action => 'new'
                                                  ),
                     :html => { :id => 'expense-form' } do |f| %>
    <label for="expense_description">Description:</label><br />
    <%= f.text_field 'description', :size => 60 %><br />
    
    <label for="expense_amount">Amount:</label><br />
    <%= f.text_field 'amount', :size => 10 %><br /><br />
    
    <%= submit_tag 'Add Expense' %>
  <% end %>
</div>

This isn't a regular form. We've used the form_remote_for() method, which posts the data from the form to our controller action in the background with an Ajax request. The first parameter passed is the name of the object; it is the key under which the form data will be located in the params Hash. The second parameter is the object that provides the form's initial values. Then we pass in the :url option, which tells the form where to post the form data. Notice we call the routing helper method hash_for_expenses_url(), which is generated based on our named route expenses in config/routes.rb. Calling this method saves us from the hassle of specifying the controller name in the :url Hash. We also gave the form an id of expense-form so that it can be referenced from our RJS templates and other JavaScript code.

Finally, let's create the RJS template that updates our project's expense page when we add a new expense. Create app/views/expenses/new.rjs and add the following code to it:

page.insert_html :bottom, 'expenses', :partial => 'expense'
page.visual_effect :highlight, "expense-#{@expense.id}"
page.form.reset 'expense-form'

The first line inserts the HTML rendered by the partial, _expense.html, into the DOM element with id expenses. The option :bottom specifies that the HTML from the partial template will be inserted inside the element, but after the element's existing content. Since there is an instance variable @expense and the partial template is also named expense, @expense automatically becomes available within the partial as the local variable expense, as though we passed in the option :object => @expense.

The second line applies the Scriptaculous visual effect Highlight to the new Expense object. Finally, the third line resets the expense form using an RJS class proxy. Next, we'll give the Expense Tracker a test drive and see what happens.

What We Have So Far

Now that everything is ready, let's try out what we have so far. Go ahead and add a new expense to the project. I added the new keyboard I bought this morning and captured the output (see Figure 3).

Updating and highlighting without a page refresh.

Figure 3. Updating and highlighting without a page refresh.

Wow, that was so easy. All of that functionality and we didn't even have to write a single line of JavaScript. The exciting part is that this is just beginning! The new expense form is pretty cool, but it would be great to improve the form to show the total expenses or some other interesting summary information. The form could also provide some sort of feedback when a remote request is being processed. It would also be nice to disable the form so that the user doesn't accidentally click the Add Expense button more than once.

So far we've done a lot of standard Rails tasks and just added a sprinkle of RJS in for flavor. Next, we're going to take a look at FireBug. FireBug is an invaluable tool in any Ajax developer's toolkit. Getting familiar with it will allow you to see exactly what is going in every Ajax request made throughout the rest of the document.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.56.12