Updating Page Elements with RJS Templates

Problem

You want to update multiple elements of the DOM with a single Ajax call. Specifically, you have an application that lets you track and add new tasks. When a new task is added, you want to be able to update the task list, as well as several other elements of the page, with a single request.

Solution

Use the Rails JavaScriptGenerator and RJS templates to generate JavaScript dynamically, for use in rendered templates.

To start, include the Prototype and script.aculo.us libraries in your layout:

app/views/layouts/tasks.rhtml:

<html>
<head>
 <title>Tasks: <%= controller.action_name %></title>
 <%= javascript_include_tag :defaults %>
 <%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<p ><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>

The index view displays the list of tasks by rendering a partial; form_remote_tag helper sends new tasks to the server using the XMLHttpRequest object:

app/views/tasks/index.rhtml:

<h1>My Tasks</h1>
<div id="notice"></div>
<div id="task_list">
 <%= render :partial => 'list' %>
</div>
<br />
<% form_remote_tag :url => {:action => 'add_task'} do %>
 <p><label for="task_name">Add Task</label>;
 <%= text_field 'task', 'name' %></p> 
 <%= submit_tag "Create" %>
<% end %>

The _list.rhtml partial iterates over your tasks and displays them as a list:

app/views/tasks/_list.rhtml:

<ul>
<% for task in @tasks %>
 <% for column in Task.content_columns %>
 <li><%=h task.send(column.name) %></li>
 <% end %>
<% end %>
</ul>

The Tasks Controller then defines the index and add_task methods for displaying and adding tasks:

app/controllers/tasks_controller.rb:

class TasksController < ApplicationController
 def index
 @tasks = Task.find :all
 end
 def add_task
 @task = Task.new(params[:task])
 @task.save
 @tasks = Task.find :all
 end end

Finally, create an RJS template that defines what elements are to be updated with JavaScript and how:

app/views/tasks/add_task.rjs:

page.replace_html 'notice', 
 "<span style=\"color: green;\">#{@tasks.length} tasks, 
 updated on #{Time.now}</span>" 
page.replace_html 'task_list', :partial => 'list'
page.visual_effect :highlight, 'task_list', :duration => 4 

Discussion

As of this writing, the JavaScriptGenerator helper is available only in Edge Rails (the most current, prerelease version of Rails).

When Rails processes a request from an XMLHttpRequest object, the action that handles that request is called and then, usually, a template is rendered. If your application contains a file whose name matches the action, ending with .rjs, the instructions in that file (or RJS template) are processed by the JavaScriptGenerator helper before rendering the ERb template. The JavaScriptGenerator generates JavaScript based on the methods defined in the RJS template file. That JavaScript is then applied to the ERb template file that initiated the XMLHttpRequest call. The result is that you can update any number of page elements with a single Ajax request, without refreshing the page.

The solution contains an Ajax form that uses the form_remote_tag helper to submit new tasks to the server using XMLHttpRequest. The :url option specifies that the add_task action is to handle these requests, which it does by creating a new task in the database. Next, the RJS template corresponding to this action is processed.

The RJS template file (add_task.rjs) contains a series of methods called on the page object. The page represents the DOM that is to be updated. The first call is page.replace_html, which acts on the element in the DOM with an ID of notice, and replaces its contents with the HTML supplied as the second argument. Another call to page.replace_html replaces the task_list element with the output of the _list.rhtml partial. The final method, page.visual_effect, adds a visual effect that indicates a change has occurred by momentarily highlighting the task_list element with a yellow background.

shows the results of adding a new task.

Figure 8-5. Using RJS templates to update several elements of a page with one Ajax request