Enhancing the Expense Tracker

The Expense Tracker currently uses an Ajax call in the background to add Expense objects to a Project. Although the form works and successfully adds expenses to a project, it could really use some enhancements. Next we're going to add an activity indicator to the page and later on we'll add a summary section that shows some statistics about the project.

Ajax Activity Indicator

One problem with Ajax is that it break a user's assumptions about how his web browser works. The user is used to having the entire page reload after performing an action that interacts with the server. With no indication that the page is busy, the user is left wondering what is going on. The user may also think that nothing is happening and repeatedly click the link or button, causing undesired effects.

One solution to this problem is to place some kind of indicator on the page that lets the user know that a remote call is in progress. In this example we'll use an animated GIF, but some descriptive text is also used. We can also disable the form while the request is in progress to prevent the user from accidentally clicking the submit button more than once.

We can do all of this by hooking into the JavaScript callbacks offered by the Ajax request. Rails lets you hook into the callbacks by passing in options to the remote call. The available callbacks are: :uninitialized, :loading, :loaded, :interactive, :complete, :failure, and :success. See the Rails documentation for more information regarding these callbacks.

Create the file public/javascripts/application.js if it doesn't already exist and add the following code to it:

var ExpenseTracker = {}

ExpenseTracker = {
  disableExpenseForm: function() {
    Element.show('form-indicator'),
    Form.disable('expense-form'),
  },
  
  enableExpenseForm: function(form) {
    Element.hide('form-indicator'),
    Form.enable('expense-form'),
  }
}

We've created a new JavaScript object ExpenseTracker that will be available from our page. We then added a simple method, disableExpenseForm(), that shows a spinning indicator and disables the form while the Ajax request is loading. We also added enableExpenseForm(), which hides the indicator and enables the form when the request is complete. We could have called these methods directly without the ExpenseTracker object, but we'll be adding more functionality to each method in the future and it is nice to keep the functionality encapsulated in one place. I also like the fact that the code that manages the form won't be cluttering up the RJS templates.

Since we used javascript_include_tag :defaults in our layout, Rails is smart enough to include public/javascripts/application.js along with the Rails JavaScript libraries. Now that our simple JavaScript functions are ready to go, we can hook them into the callbacks offered by the remote request. Open up app/views/expenses/_new.rhtml and modify it to include the callbacks. The form_remote_for() method call should look like this when you've finished:

<% form_remote_for :expense,
                   Expense.new,
                   :url => hash_for_expenses_url(:project => @project, :action => 'new'),
                   :loading => 'ExpenseTracker.disableExpenseForm()',
                   :complete => 'ExpenseTracker.enableExpenseForm()',
                   :html => { :id => 'expense-form' } do |f| %>

The form is given the id expense-form so that we can refer to it within the JavaScript function. We could achieve the same results that we get from the enableExpenseForm() method by calling the equivalent methods in the RJS template, but it is nice to keep all of the callback code in the ExpenseTracker object. This also keeps the mechanics of managing the form from cluttering up the RJS templates. We could also reset the form in the enableExpenseForm() method, but this would always reset the form. We want to be able control when the form is reset so that the user doesn't have to re-enter the data in the case of failing validations or other problems.

Next, add the indicator image after the submit_tag() call in app/views/expenses/_new.rhtml. Set the initial style to display:none so that the indicator isn't visible when the page is first loaded. My indicator is just a simple animated GIF that mimics Mozilla Firefox's spinning indicator. I placed the image in public/images folder so that it is available to our project.

<%= image_tag 'indicator.gif', :id => 'form-indicator', :style => 'display:none;' %>

Now when submitting the form, the browser displays the spinning indicator image and disables the form (see Figure 5). This technique offers a visual cue that an Ajax request is in progress and prevents the user from submitting the new expense multiple times. The indicator is hidden and the form re-enabled when the request has completed. Since the entire process occurs very quickly in the normal case, it can be very hard to see that the form is being disabled and the indicator shown. You can slow things down a bit by putting a call to sleep() in the new() action of the ExpenseController. Obviously you'd only want to do this in development to test that the process is actually working. The controller code with a call to sleep() is shown in the following sample:

class ExpensesController < ApplicationController
  before_filter :find_project

  def new
    @expense = @project.expenses.create(params[:expense])
    # Sleep for 3 seconds
    sleep 3
  end

  private
  def find_project
    @project = Project.find(params[:project])
  end
end
The expense form is disabled and an indicator is shown while the Ajax request is in progress.

Figure 5. The expense form is disabled and an indicator is shown while the Ajax request is in progress.

Ajax Global Responders

The :loading and :complete callbacks of the form_remote_tag() worked very well for showing and hiding the form indicator. The only problem is that if you have a lot of Ajax functionality built into your page, it can be tedious to add the image and wire up the :loading and :complete callbacks for every remote operation. This is where the Ajax global responders come in handy.

The Prototype Global Ajax Responders are a great place to register JavaScript functions that you would like to have executed on every Ajax request. The Ajax global responders are provided by the Prototype library; they allow you to hook functions into the various callbacks of all Ajax requests. Let's move code that shows and hides the indicator image out of the :loading and :complete callbacks and instead use the Ajax global responders.

Instead of placing an indicator image beside every form or element that makes Ajax calls, the Ajax Global Responders allow you to set up a single indicator on the page that will be shown during any Ajax request made from the page.

Setting up the Global Responders is really simple. You can add the code to wire up a global response in public/javascripts/application.js:

Ajax.Responders.register({
 onCreate: function() {
   if (Ajax.activeRequestCount > 0)
     Element.show('form-indicator'),
 },
 onComplete: function() {
   if (Ajax.activeRequestCount == 0)
     Element.hide('form-indicator'),
 }
 });

The code is straightforward. Ajax.Responders.register() takes an anonymous JavaScript object, where the property name is the name of the Ajax callback and the value is a JavaScript function. We are wiring up a function that will be executed on every onCreate() callback and another function that will be executed on every onComplete() callback. The first function shows the DOM element with id form-indicator when there is one or more active Ajax Request. The second function hides the indicator when there are no active Ajax requests processing.

We can now either remove the lines that show and hide the form indicator image from the ExpenseTracker object in public/javascripts/application.js or we can remove the ExpenseTracker JavaScript code altogether and just write the code inline in the callback. This is what the form_remote_for call in app/views/expenses/_new.rhtml would look like if we eliminated the ExpenseTracker JavaScript object and simply wrote the code inline:

<% form_remote_for :expense,
                   Expense.new,
                   :url => hash_for_expenses_url(:project => @project, :action => 'new'),
                   :loading => 'Form.disable("expense-form")',
                   :complete => 'Form.enable("expense-form")',
                   :html => { :id => 'expense-form' } do |f| %>

Now the callbacks only enable and disable the form. The code to show and hide the indicator image is being executed by the Ajax Global Responders. The indicator appears whenever there is any Ajax activity and is hidden when all requests have completed. When we add more features, it is a good idea to move the indicator image to another part of the screen, or use an animated LightBox image. For now, we'll just leave the image in the same location.

Ajax Global Responders offer a great way to perform actions during the life cycle of every Ajax request. This not only cuts down on code duplication, which makes our templates more easily understandable, but also saves a whole lot of typing.

Model Validations

In its current state, the Expense Tracker will accept any input and try to create Expense objects. The problem is that the application chokes on invalid input. The most likely case is ActiveRecord throwing an exception that isn't caught by our code. Our newly-added Ajax indicator will just keep spinning away and the user won't know what happened. Lucky for us, Rails has wonderful support model validations. We can validate the new Expense objects and return a nice alert box to the user showing any problems. Let's add some validations to the Expense model in app/models/expense.rb:

class Expense < ActiveRecord::Base
  belongs_to :project

  validates_presence_of :description
  validates_numericality_of :amount

  protected
  def validate
    errors.add(:amount, "must be greater than 0") unless amount.nil? || amount >= 0.01
  end
end

This validation code will ensure that the description is not blank and that the amount of the Expense is a number greater than 0. Now we just have to modify our RJS template slightly to display the errors. Open up app/views/expenses/new.rjs and modify the template to look like the following:

if @expense.new_record?
  page.alert "The Expense could not be added for the following reasons:
" + 
             @expense.errors.full_messages.join("
")
else
  page.insert_html :bottom, 'expenses', :partial => 'expense'
  page.visual_effect :highlight, "expense-#{@expense.id}"
  page.form.reset 'expense-form'
end

The code checks to see if the Expense object is still a new record. If it is still a new object, then there must have been a problem saving it and the errors are shown. Otherwise, the normal action of inserting and highlighting the new Expense is performed. Notice that the form is only reset when the operation was successful. This way the user doesn't have to retype the description and amount when there are errors.

In this case we just used a simple JavaScript alert box to show the errors. This is the simplest method of displaying the errors with RJS. Another solution would be to replace the entire form and insert the rendered output of error_messages_for() into the page. This would take advantage the built-in Rails helpers, but also has more overhead in the RJS templates, as you would have to remove or hide the rendered error section after the Expense object was successfully added.

Adding Some Calculations

Since the Expense form is disabled when a request is being processed, it is a lot more usable. But there's a long way to go. I still don't know what my total expenses for the project are. Also, while we're at it, let's add some code that displays other interesting data, such as the minimum expense, maximum expense, and the average expense of the project. We'll have to make sure that all this additional information gets updated as we add expenses to the project.

First, we need to add these calculation methods to our Project model. Open up app/models/project.rb and add the calculation methods. Your model should look something like the this:

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

  def total_expenses
    expenses.sum(:amount)
  end

  def min_expense
    expenses.minimum(:amount)
  end

  def max_expense
    expenses.maximum(:amount)
  end

  def avg_expense
    expenses.average(:amount)
  end
end

These methods are all ridiculously simple. We use the power of the new Active Record Calculations (added in Rails 1.1) to do all of the dirty work. Notice that the calculation methods are being called from the expenses collection. Calling each calculation from the collection instead of from the Expense class causes the calculation to be scoped to the current Project, which is what we want in this case. We pass in the Symbol :amount to each calculation because that is the Expense attribute on which we want to perform the calculation.

We might as well display all of this information on the page that shows the project's expenses. We can show the total expenses using a partial that we'll render directly under the list of expenses. Create app/views/expenses/_total.rhtml, which will look like this:

<table id="total">
  <tr>
    <td></td>
    <td class="total">Total</td>
    <td id="total-amount" class="amount"><%= number_to_currency(total) %></td>
  </tr>
</table>

number_to_currency() is another Rails numerical helper method. It formats the amount passed to it with two decimal places and places a dollar sign before the number. What about the other calculations? We can place them in a table above the list of expenses. Create another partial named app/views/expenses/_summary.rhtml, and add the summary table.

<table id="summary">
  <tr>
    <td>Min expense</td><td class="amount"><%= number_to_currency(min) %></td>
  </tr>
  <tr>
    <td>Max expense</td><td class="amount"><%= number_to_currency(max) %></td>
  </tr>
  <tr>
    <td>Ave expense</td><td class="amount"><%= number_to_currency(average) %></td>
  </tr>
</table>

Now we need to render the partials we just created. Open up app/views/projects/show.rhtml and add the lines that render the new partials. The view should look like this:

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

<h2>Summary</h2>
<%= render :partial => 'expenses/summary', 
           :locals => { :min => @project.min_expense, 
                        :max => @project.max_expense, 
                        :average => @project.avg_expense } %>

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

We added an <h2> header tag for the summary and rendered the summary partial underneath it. We pass the minimum, maximum, and average expense values into the partial as local variables in the locals Hash. Then, after the list of expenses, we render the total table. Notice that we pass @project.total_expenses into the _total.rhtml partial as the value of the :object key. This makes the value @project.total_expenses available in the partial as the local variable total, which is also the name of the partial template.

At this point, we just need to add the code that updates the total expenses and the new summary table. Let's edit the RJS template app/views/expenses/new.rjs again and add the following code:

if @expense.new_record?
  page.alert "The Expense could not be added for the following reasons:
" +
             @expense.errors.full_messages.join("
")
else
  page.replace 'summary', :partial => 'summary', 
                          :locals => { :min => @project.min_expense, 
                          :max => @project.max_expense, 
                          :average => @project.avg_expense }
  page.insert_html :bottom, 'expenses', :partial => 'expense'
  page.visual_effect :highlight, "expense-#{@expense.id}"
  page.replace_html 'total-amount', number_to_currency(@project.total_expenses)
  page.form.reset 'expense-form'
end

First we are replacing the summary table with the rendered _summary.rhtml partial. Since we're using replace, the entire summary element is replaced and not just its contents. Again, like in show.rhtml, we pass in the :locals Hash that contains the calculations. Then we apply a Highlight effect to the summary. Next, we replace the contents of the total-amount element with the updated total amount. This time we call replace_html, which replaces the innerHTML of the of the DOM element whose id is specified as the first parameter.

Now we have much more functionality in our page. It is great to have summary calculations and total expenses update automatically. It is amazing that all of this can be done with so little code (see Figure 6).

Instant gratification─updating multiple page elements with new data.

Figure 6. Instant gratification─updating multiple page elements with new data.

Refactoring with RJS Helpers

Now that the new expenses are being correctly inserted and the total is properly updated, we can go ahead and refactor the RJS template using the RJS helper methods. The separate tasks are displaying the errors to the user, updating the summary, inserting and highlighting a new expense, and updating the total amount. Let's create helpers for these. Open up app/helpers/expenses_helper.rb and add the following code:

module ExpensesHelper
  def display_errors(expense)
    page.alert "The Expense could not be added for the following reasons:
" + 
               expense.errors.full_messages.join("
")
  end

  def update_summary(project)
    page.replace 'summary', :partial => 'summary', 
                            :locals => { :min => project.min_expense, 
                                         :max => project.max_expense, 
                                         :average => project.avg_expense }
  end

  def insert_expense(expense)
    page.insert_html :bottom, 'expenses', :partial => 'expense', :object => expense
    page.visual_effect :highlight, "expense-#{expense.id}"
  end

  def update_total(amount)
    page.replace_html 'total-amount', amount
  end
end

Now edit app/views/expenses/new.rjs to look like this:

if @expense.new_record?
  page.display_errors @expense
else
  page.update_summary @project
  page.insert_expense @expense
  page.update_total number_to_currency(@project.total_expenses)
  page.form.reset 'expense-form'
end

That certainly makes the code a lot cleaner. I placed the call to number_to_currency() outside the helper because the ActionView::Helpers modules are not included in the context in which the RJS helper executes. To move the number_to_currency call into the helper, you have to include ActionView::Helpers::NumberHelper in the helper module.

module ExpensesHelper
  include ActionView::Helpers::NumberHelper

  def display_errors(expense)
    page.alert "The Expense could not be added for the following reasons:
" +
               expense.errors.full_messages.join("
")
  end

  def update_summary(project)
    page.replace 'summary', :partial => 'summary', 
                            :locals => { :min => project.min_expense, 
                                         :max => project.max_expense, 
                                         :average => project.avg_expense }
  end

  def insert_expense(expense)
    page.insert_html :bottom, 'expenses', :partial => 'expense', :object => expense
    page.visual_effect :highlight, 'expense-#{expense.id}'
  end

  def update_total(amount)
    page.replace_html 'total-amount', number_to_currency(amount)
  end
end

The RJS helpers really help make the code in our RJS template a lot simpler. Obviously, there isn't a lot of value in extracting the method calls into helpers if the code is only called in one place. However, the helpers are great when the code in your RJS templates is being duplicated in several different templates. The helpers methods are also available to inline RJS calls from your controllers when you are using render :update. If you are duplicating code in your RJS templates and inline RJS calls, you can probably benefit from extracting that functionality into helpers.

A Look Ahead

As you can see from the Expense Tracker, it is almost too easy to implement complex Ajax features with Rails and RJS templates. The Expense Tracker just barely scratches the surface of what is possible. The RJS Reference section has examples of usage so you can master all aspects of RJS for your own projects. Have fun!

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

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