#43 AJAX with RJS
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.
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:
- Where to insert the HTML (this can be
:before
,:after
,:top
or:bottom
). - The
id
of the element we want to modify (our ‘reviews’ list). - The HTML to render. This can be as simple as a string, or, in our case, a partial. We pass the name of the partial and the object to pass to it. We’re rendering a new review, so we pass the
review
partial and the new review object we created. (In Rails 2.3 this can be shortened to:partial => @review
).
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