homeASCIIcasts

43: AJAX with RJS 

(view original Railscast)

Other translations: It

In this episode we’re going to use RJS to add some AJAX functionality to a site. Using RJS is the easiest way of adding AJAX to a Rails site, especially if you need to update multiple elements on a page.

On our site’s products page, users can add reviews for a product via a form. When the form is submitted it posts back to the server via an HTTP POST request and the page is reloaded.

Our product page showing the reviews.

Our product page showing the reviews.

A number of elements on the page are different after the form has been submitted: the text showing the number of reviews has changed, the review has been added to the list, the form has been reset and there is a message at the top of the page thanking the user for adding their review. All of these will need to be updated by our RJS template after we AJAXify the form.

Modifying The View

Before we start updating the view code we first need to make sure that we’re including prototype and the other standard JavaScript files. To do that we’ll need to add this line in the <head> section of our layout file.

<%= javascript_include_tag :defaults %>

We’ll start by modifying the view code. The view code for the product page has a standard Rails form in it.

<% form_for [@product, @review] do |form| %>
<ol class="formList">
  <li><%= form.label :name, "Name:" %> <%= form.text_field :name %></li>
  <li><%= form.label :content, "Review:" %> <%= form.text_area :content, :rows => 5 %></li>
  <li><%= submit_tag "Add comment" %></li>
</ol>
<% end %>

All we need to do to make the form post back via AJAX is to replace form_for in the first line with form_remote_for. Rails provides a number of methods that turn ‘normal’ elements into AJAX-enabled ones; for more information there is a list avaiable on the Rails API site1.

After making the change to our form and refreshing the page the opening form tag will now have an onsubmit attribute that will make the form be submitted asynchronously. If the site is being used by someone who doesn’t have JavaScript enabled on their browser then the form will still work, but the page will post back in the same way it did before we added the AJAX code.

<form action="/products/1/reviews" class="new_review" id="new_review" method="post" 
onsubmit="new Ajax.Request('/products/1/reviews', 
{asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); 
return false;”>

Modifying The Controller

The review form is submitted to an action called create in the reviews controller. The action creates the new review, sets a flash notice and then redirects back to the product’s page.

def create
  @review = Review.new(params[:review])
  @review.product_id = params[:product_id]
  @review.save
  flash[:notice] = "Thanks for your review!"
  redirect_to product_path(params[:product_id])
end

Redirecting won’t work with an AJAX request, so we’re going to have to modify the controller to respond differently depending on whether an HTTP request or a JavaScript request has been made. The respond_to method lets us do this. It takes a block in which we put the code for each different format. We’ll replace the redirect_to line in the action above with this:

respond_to do |format|
  format.html { redirect_to product_path(params[:product_id]) }
  format.js
end

Now, with the code above, the redirect will still take place if we call the action via HTTP, but not if we call it via AJAX. As there’s no code in the js block it will fall through to the RJS template.

Creating the RJS template

The template file goes in the /app/views/reviews folder and, as it’s executed by the create action, is called create.rjs. The RJS file will generate JavaScript and return it to the client in response to the AJAX request.

The first thing we need to do is to add the new review to the list. The reviews are rendered as an ordered list with an id of reviews. Each review is rendered in a partial called _review.html.erb. The RJS to add the new review to the bottom of the list is this.

page.insert_html :bottom, :reviews, :partial => 'review', :object => @review

The code above uses insert_html to add HTML to the page. The arguments it takes are:

The new review will now be added to the list when we submit the form, but the text that shows the number of reviews will not be updated. We’ll add some more RJS code to do that. This time we’re replacing HTML rather than inserting so we’ll use replace_html.

page.replace_html :reviews_count, pluralize(@review.product.reviews.size, 'Review')

With replace_html we need to pass the id of the element we want to update and the new content. We’ll pass the text back using the same pluralize method we use in the view code. As we don’t have access to the @product variable we use in the view we have to get the new review’s product and then the number of reviews it has.

We’ll also want to reset the form when a review is added. The form has an id of new_review and we can use the page object to pass a JavaScript method to it like this.

page[:new_review].reset

Lastly, we need to show the flash notice. As with the element that shows the number of reviews, we can use replace_html to show the message.

page.replace_html :notice, flash[:notice]

One Final Problem

Our form now works as we want it to and reviews can be added without the reviews page posting back to the server and refreshing itself. There is one small problem remaining, however. If we refresh the page after we’ve added a comment the flash message remains, although it will disappear if the page is refreshed again. This is because of the way flash works: it will hold on to the message for one request.

The way to work around this is to discard the flash after it is passed to the JavaScript for rendering. We just need to add one more line to our RJS file.

flash.discard

Now, the flash will not appear again if we refresh the page or navigate to another one.

Our page now behaves exactly as it did before but without having to post back. The final RJS file looks like this.

page.insert_html :bottom, :reviews, :partial => 'review', :object => @review
page.replace_html :reviews_count, pluralize(@review.product.reviews.size, 'Review')
page[:new_review].reset
page.replace_html :notice, flash[:notice]
flash.discard