Using JQuery instead of Prototype and RJS for Ajax with Rails
Basically notes taken from Ryan Bates' Railscast http://media.railscasts.com/videos/136_jquery.mov,
annotated with bits from the JQuery documentation http://docs.jquery.com
JQuery encourages unobstrusive javascript.
Using JQuery means not using Rails related helper methods (form_remote_for, etc) and RJS.
The JRails plugin (JQuery on Rails)
http://ennerchi.com/projects/jrails
is a drop-in jQuery replacement for Prototype/script.aculo.us on Rails.
JRails provides all of the same default Rails helpers for javascript functionality using JQuery.
The issue is that the JRails approach is not in line with the JQuery philosophy of unobstrusive javascript, because the Rails helper methods are obstrusive since they inject javascript into the generated html.
Example: Add review via form on Product page.
The product page template show.html.erb looks like:
<% title @product.name %>
<p>Price: <%= number_to_currency @product.price %></p>
<h3 id="reviews_count">
<%= pluralize(@product.reviews.size, "Review") %>
</h3>
<div id="reviews">
<%= render :partial => @product.reviews %>
</div>
<h3>Add a Review</h3>
<% form_for Review.new(:product => @product) do |f| %>
<%= f.hidden_field :product_id %>
<p>
<%= f.label :name %><br />
<%= f.text_field :name %>
</p>
<p>
<%= f.label :content, "Review" %><br />
<%= f.text_area :content, :rows => 8 %>
</p>
<p><%= f.submit "Add Review" %></p>
<% end %>
The goal is to have 4 actions to be done via Ajax
1. add flash message
2. update review count
3. append review to reviews section
4. clear the form
Add JQuery javascript file include to head section of layout
If the jquery javascript library file is in public/javascripts/jquery.js,then add the following to the head section of layout.
<%= javascript_include_tag 'jquery', 'application' %>
The jquery file needs to be included before the file where the jquery library functions are to be invoked, in this case "application.js".
Unobstrusive Javascript
The approach is to use standard html and add behavior dynamically through javascript.Can use Rails helpers such as form_for that generates standard html but don't use remote_form_for and other Ajax helpers that generate javascript as well.
Add JQuery code to the included application.js file.
The above view template will generate a form tag like:
<form id="new_review" class="new_review" method="post" action="/reviews">
Take over the submit function on the form with
$("#new_review").submit(function() {
$.post($(this).attr("action"),
$(this).serialize(), null, "script");
return false; // so form doesn't get submitted normally
})
$.post is a simplified wrapper function for $.ajax and takes 4 parameters:
url - the URL of page to load, $(this).attr("action") is the form's "action" attribute, in this case: action="/reviews"
data - $(this).serialize() serializes form input elements into a string of data
callback - function to execute on success, in this case set to null because it uses the type option to handle returned javascript.
type - "script" option means that returned data will be processed as javascript and mimics behavior of Rails RJS.
Setting request type
JQuery needs to tell Rails that the request is an Ajax request not an HTML request (the default).There are 2 ways to do this:
1. add .js extension to url, e.g. $(this).attr("action") + '.js'
2. change the request header attribute "Accept" to "text/javascript", using the jQuery.ajaxSetup method to setup global settings for AJAX requests.
So in application.js, before $(document).ready add:
jQuery.ajaxSetup({
'beforeSend': function(xhr) {
xhr.setRequestHeader("Accept", "text/javascript") }
})
Rails Server Side Handling
Controller
Respond differently depending on if it's normal html request or Ajax request.
class ReviewsController < ApplicationController
def create
@review = Review.create!(params[:review])
flash[:notice] = "Thank you for reviewing this product"
respond_to do |format|
format.html { redirect_to @review.product }
format.js
end
end
end
View
Use .erb template instead of .rjs because RJS is used with Prototype (unless using JRails plugin).
So create the template create.js.erb containing javascript to be executed on browser side.
1. add flash notice
$("#new_review").before('<div id="flash_notice">
<%= escape_javascript(flash.delete(:notice)) %></div>');
- escape_javascript escapes single and double quotes and carrier returns to prevent javascript getting messed up.
- flash.delete deletes the :notice key/value pair and returns the value for the erb tag. The :notice key/value pair needs to be deleted so that it does not carry over to the next request.
2. update reviews count
$("#reviews_count").html("<%= pluralize(@review.product.reviews.count, 'Review') %>");
- .html sets the html contents of the matched elements
3. append new review to reviews
$("#reviews").append("<%= escape_javascript(render(:partial => @review)) %>);
- .append appends content to matched elements.
4. reset the form
$("#new_review")[0].reset();
- reset: jQuery returns a collection of dom elements. To operate on the DOM element itself, it needs to be extracted from the collection.
jQuery('#myForm').get(0).reset();
or a little shorter
Template code in entirety:
$("#new_review").before('<div id="flash_notice"><%= escape_javascript(flash.delete(:notice)) %></div>');
$("#reviews_count").html("<%= pluralize(@review.product.reviews.count, 'Review') %>");
$("#reviews").append("<%= escape_javascript(render(:partial => @review)) %>");
$("#new_review")[0].reset();
Extending JQuery and Adding Functions
Can create functions to be invoked from the collection of matching elements.
For example, the code that takes over the form submit function can be refactored as a function:
jQuery.fn.submitWithAjax = function() {
this.submit(function() {
$.post($(this).attr("action"), $(this).serialize(),
null, "script");
return false;
})
}
To be invoked like:
$("#new_review).submitWithAjax();Rails Authenticity token with JQuery
Rails 2 protects against CSRF attacks.Rails form helpers generates a hidden input field for the authenticity_token.
If the rails helpers are not used for non-get requests, then the authenticity_token has to be added as a parameter to the request.
See: http://henrik.nyh.se/2008/05/rails-authenticity-token-with-jquery
References
http://www.loudthinking.com/posts/32-myth-3-rails-forces-you-to-use-prototype outlines basically the same approach as the above.
http://codetunes.com/2008/12/08/rails-ajax-and-jquery/ describes a general approach for adding ajax behavior to links and posts. Be sure to read the comments, including the comment on no longer needing the livequery plugin because of the new live event binding in jquery 1.3
http://docs.jquery.com/Events/live
No comments:
Post a Comment