18 May 2009 ~ 30 Comments

Beginners guide to Jquery + Ruby On Rails

First time I wanted to use Jquery with Ruby on Rails I had some difficulties understanding how it can works.

Sure, you can easily use Jrails, that’ll brings you RJS helpers with Jquery, but I quite like the unobstrusive way, so Jrails is not (for me) the best solutions.

Let’s code together

What do we want?

We are going to simulate a blog post comment system. We need to be able to list the comments/create a new comment/rate a comment/delete a comment.

All this need to work with and without JavaScript enabled.

I deliberately didn’t use any user/role filtering, the goal of the code is to show of to work with Rails and Jquery

Step1: Prepare the app

We want to work with Jquery, so we don’t need the Prototype files anymore. Just remove everything from your public/javascript/ appart application.js.

In your master layout add this bunch of lines

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js"></script>
        <% if protect_against_forgery? %>
          <script type='text/javascript'>
              //<![CDATA[
                window._auth_token_name = "#{request_forgery_protection_token}";
                window._auth_token = "#{form_authenticity_token}";
            //]]>
            </script>
        <% end %>
        <%= javascript_include_tag 'application' %>

We are linking Jquery to the google hosted version, and calling the application.js and setting the forgery protection.

Then in your application.js copy and past the content of my default application.js (Also available as a gist)

jQuery.ajaxSetup({ 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")} })

function _ajax_request(url, data, callback, type, method) {
    if (jQuery.isFunction(data)) {
        callback = data;
        data = {};
    }
    return jQuery.ajax({
        type: method,
        url: url,
        data: data,
        success: callback,
        dataType: type
        });
}

jQuery.extend({
    put: function(url, data, callback, type) {
        return _ajax_request(url, data, callback, type, 'PUT');
    },
    delete_: function(url, data, callback, type) {
        return _ajax_request(url, data, callback, type, 'DELETE');
    }
});

jQuery.fn.submitWithAjax = function() {
  this.unbind('submit', false);
  this.submit(function() {
    $.post(this.action, $(this).serialize(), null, "script");
    return false;
  })

  return this;
};

//Send data via get if JS enabled
jQuery.fn.getWithAjax = function() {
  this.unbind('click', false);
  this.click(function() {
    $.get($(this).attr("href"), $(this).serialize(), null, "script");
    return false;
  })
  return this;
};

//Send data via Post if JS enabled
jQuery.fn.postWithAjax = function() {
  this.unbind('click', false);
  this.click(function() {
    $.post($(this).attr("href"), $(this).serialize(), null, "script");
    return false;
  })
  return this;
};

jQuery.fn.putWithAjax = function() {
  this.unbind('click', false);
  this.click(function() {
    $.put($(this).attr("href"), $(this).serialize(), null, "script");
    return false;
  })
  return this;
};

jQuery.fn.deleteWithAjax = function() {
  this.removeAttr('onclick');
  this.unbind('click', false);
  this.click(function() {
    $.delete_($(this).attr("href"), $(this).serialize(), null, "script");
    return false;
  })
  return this;
};

//This will "ajaxify" the links
function ajaxLinks(){
    $('.ajaxForm').submitWithAjax();
    $('a.get').getWithAjax();
    $('a.post').postWithAjax();
    $('a.put').putWithAjax();
    $('a.delete').deleteWithAjax();
}

$(document).ready(function() {

// All non-GET requests will add the authenticity token
  // if not already present in the data packet
 $(document).ajaxSend(function(event, request, settings) {
       if (typeof(window.AUTH_TOKEN) == "undefined") return;
       // IE6 fix for http://dev.jquery.com/ticket/3155
       if (settings.type == 'GET' || settings.type == 'get') return;

       settings.data = settings.data || "";
       settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(window.AUTH_TOKEN);
     });

  ajaxLinks();
});

This is my default application.js when I want to work with Jquery.

Basically what brings you these functions are :

Any link for a class get/post/put/delete will be called via ajax only if JavaScript enabled. Same for form with ajaxForm class.

The code

You can find the original code on my github at

http://github.com/spyou/railsjquery/tree/master

I’ve created a comment resource. The comments table has a body and a score field.

Here is the comments_controller.rb

class CommentsController < ApplicationController
  #This is a virtual comment manager. There is no user detection, no session
  before_filter :load_post

  #This is our entry point, I'm just going to display a factice post to comment
  def index
    @comments = Comment.all
    respond_to do |format|
      format.html #we only respond to the html request
    end
  end

  def new
    @comment = Comment.new
    if !request.xhr?
      @comments = Comment.all
    end

    respond_to do |format|
      format.html
      format.js {render :layout => false}
    end
  end

  def create
    @comment = Comment.create(params[:comment])
    respond_to do |format|
      format.html { redirect_to comments_path() }
      format.js {render :layout => false}
    end
  end

  #This is not a "normal" update, I'll use this one to add points to the comment
  def update
    @comment = Comment.find(params[:id])
    @comment.score = @comment.score + 1
    @comment.save

    respond_to do |format|
      format.html { redirect_to comments_path }
      format.js {render :layout => false}
    end
  end

  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy

    respond_to do |format|
      format.html { redirect_to comments_path }
      format.js {render :layout => false}
    end
  end

  protected

  def load_post
   @post = "This is a factice post, I hope you like it. Just comment on the bottom!"
  end
end

As you can see on it, we handle 2 different types of responses

    respond_to do |format|
      format.html
      format.js {render :layout => false}
    end

Format.html will load your [actionName].html.erb, but the format.js will load a page called [actionName].js.erb were you can put all the callback javascript you need.

For example here is my index.html.erb

    <%=
        #Use a helper to display the current comments of the post
        comments_display(@comments)
    %>
<div class="newComment">
    <%= link_to 'Create a new comment', new_comment_path(),:class => "get"%>
</div>

and the _comment.html.erb

<div id="comment<%= comment.id %>">
<%=
    content_tag(:p,
    comment.body)
%>
<!-- We are accessing the update function so we use put as class -->
<span class="score"><%= comment.score %></span><span> - <%= link_to 'Add one point!',comment_path(comment),:class => "put"  %></span>
<p> - <%= link_to '*Delete this comment',comment_path(comment),:method => :delete,:class => "delete"  %></p>
<p><%= comment.created_at %></p>
<hr />
</div>

You can see the use of get/put/delete class.

Here is now the new.js.erb (Where I call a _form.html.erb to render the new comment form)

$(".newComment").html("<%= escape_javascript(render :partial => 'form') %>");
ajaxLinks();

And here is the update.js.erb (Where I’m suppose to update the score of the comment without refresh)

//Update the score
$('#comment<%= @comment.id %> span.score').html("<%= @comment.score %>");

The tests

Well, the problem with the test is, you can test the rendering of you page (in plain text) but you can’t be sure of the javascript execution.

I’m made some simple tests (testing the plain text) that you can find in the test/functional folder.

(Example with the new comment method)

#Testing the comment creation
  context "handle :create" do
    context "by html" do
        setup do
           post :create, :comment => { :body => "My comment" }
        end
          should_change "Comment.count", :by => 1
          should_redirect_to("the index"){comments_path()}
      end
      context "by xhr" do
        setup do
           xhr :post, :create, :comment => { :body => "My xhr comment" }
        end
          should_change "Comment.count", :by => 1

          should "Display the new comment link" do
            assert_match(/new comment/,@response.body)
          end

          should "display the created new created message" do
            assert_match(%(My xhr comment),@response.body)
          end

      end
  end

This is just the beginning, I’m now looking to some Javascript testing solution for Rails, will be part of another tutorial.

In a nutshell

So in a nutshell, what’d learn here is :

  • Rails + Jquery is easy
  • format.js + file.js.erb and you’r done
  • A good application.js can save your ass

Just checkout the complete source code

git clone git@github.com:spyou/railsjquery.git
rake gems:install
rake db:create


jquery-rails-tutorial_1242648062355

I hope it was useful.

30 Responses to “Beginners guide to Jquery + Ruby On Rails”

  1. Arik Jones 18 May 2009 at 6:09 pm Permalink

    Or you can just install the jrails plugin.

  2. Nicolas 18 May 2009 at 10:17 pm Permalink

    To my opinion, Jrails is good if you have legacy code using RJS helpers that you don’t want to change. If you start a project from scratch, Jrails is going to add you some JS inside the HTML and won’t work neither is JS disabled, which is bad.

  3. Diego 4 June 2009 at 9:55 pm Permalink

    This was a great post. I’m getting into Rails development, and it’s cool to be able to set up my favorite js framework along with Rails.

    Can you elaborate on how the forgery protection works?

    Thanks!

  4. Bharat Ruparel 29 June 2009 at 2:30 am Permalink

    Excellent post. Keep them coming.
    Bharat

  5. Bharat Ruparel 29 June 2009 at 5:16 am Permalink

    Why are you not using LiveQuery plugin? What happens to the bindings when you add a new comment?
    Bharat

  6. Nicolas 29 June 2009 at 9:54 am Permalink

    I didn’t use the LiveQuery plugin here, because I wanted to show the more generic way of implementing this function into an existing Rails code. But you can modify the function and use it, I don’t think it’s a problem.
    The binding is removing action on click because when you’ll finish your action, you need to call the ajaxLinks function again, as the new links or form render are not binded because just rendered without layout.
    Hope it helps.

  7. Bharat Ruparel 29 June 2009 at 10:11 pm Permalink

    Hello Nick,
    Thanks for the explanation. Follow up questions:

    In your application.js, you are using $put and $delete_ jQuery functions. My jQuery in Action book does not show these two methods are listed, but they work fine on my Ubuntu Linux/Firefox 3.0x machine. Where can I find more information on them?

    Your example code is very useful because I learn by doing. Do you have other samples of jQuery with Rails?
    Bharat

  8. Igor 29 July 2009 at 3:21 pm Permalink

    Thanks a lot, Nicolas. I have translated this post in Russian! It is awesome!
    http://www.aleksandrov.me/?p=133

  9. leuk 31 July 2009 at 4:02 am Permalink

    good stuff guy, its rocks on my app

  10. Paul Cook 3 August 2009 at 10:04 pm Permalink

    Thanks for this very useful info. The put and delete are exactly what I was looking for.

  11. Steph 11 August 2009 at 5:13 am Permalink

    This is great stuff, really appreciate you sharing, it’s helped meget converted over to JQuery much quicker than it should have taken!
    Cheers bud!

  12. Adam Anderson 17 September 2009 at 8:05 am Permalink

    Awesome job! I’m currently using jrails on a site and feel pretty meh about it being there. I’m glad your post clearly explains how to use jQuery without jrails even though I still think it is an excellent plugin. Also that is a great convention attaching ajax to all links with classes of get/put/etc. I think if UJS is going to be accepted by more of the community then all of us proponents need to find good conventions to flatten the learning curve more and more. Keep up the great work.

  13. Simon 13 October 2009 at 4:11 pm Permalink

    As a beginner I feel that this is less of a beginner’s guide than this screencast that explains exactly what is done and instead shows only the essential stuff to get you going: http://railscasts.com/episodes/136-jquery

  14. Nicolas 14 October 2009 at 11:20 am Permalink

    @Simon, you’re right, Ryan screencast are really simple and an amazing source of inspiration. I’m sad that you found this tutorial hard to follow. Please let me know if I can help.

  15. Edward 5 November 2009 at 9:16 pm Permalink

    This is great works like a charm except when i use destroy in a list using Ryan Bates will paginate with ajax code. Once i desroy a post all the pagination links have the destroyed post_id attached to the uri and it throws off the pagination because it can no longer find a post with that id. Any ideas of how to reset the uri after a destroy

  16. Nicolas 6 November 2009 at 12:26 pm Permalink

    What you can do is reload your pagination div in the delete.js.erb. Can you share part of your code if I can help? (nicolas.alpi on gmail.com / gtalk)

  17. Ryan 4 February 2010 at 6:34 am Permalink

    Your application.js is great, I have implemented and will be cleaning mine out to accommodate yours!

    I have a question though, I am getting a 406 Not acceptable error when clicking a link with a put class.. Any idea why this could be?

  18. Ryan 4 February 2010 at 8:10 am Permalink

    Fixed my issue, its a bug with firefox 3.5 :/

  19. violet313 19 February 2010 at 10:56 pm Permalink

    nice1 Nico ;)
    works a charm!

    [quote]
    @Simon, you’re right, Ryan screencast are really simple and an amazing source of inspiration. I’m sad that you found this tutorial hard to follow. Please let me know if I can help.
    [unquote]

    hey there’s nothing wrong with ur tutorial! -the railscast /does/ fill in the gaps 4 us newbs who want2 understand it -but thx2simon it’s now referenced in the comments so np =)

  20. Nellboy 27 February 2010 at 12:02 pm Permalink

    Hi Nicolas

    really big thanks for posting this. It has been a huge help and has significantly lowered the entry barrier for using jquery effectively with Rails. Very well explained, and easy to understand.

    I have one question if you have time. I’m running into problems when making AJAX calls from within divs and partials that are shown by other AJAX calls.

    Is there a particular reason for this? I’m struggling to understand why this might happen in this way.

    cheers!

    Paul

  21. Ryan 9 March 2010 at 7:19 pm Permalink

    This works great, but I (of course) have some questions I’m hoping you can help with.

    How do you handle the error callback? I have some cases where if the action sees something wrong, or the save fails, I’d like to have a callback [action]_error.js.erb or something similar that can report to the user what happened, with the current implementation it always calls the callback, regardless of success/fail (as far as I can see). I’d like to be able to call ‘return false’ in my action, and have the error callback fired, rather than the success.

  22. Nicolas 9 March 2010 at 9:52 pm Permalink

    Hi Nellboy, thanks for the comment.

    I think you problem can be corrected by calling ajax_link:

    1. On page load (as in tutorial)
    2. On your javascript page load (exemple at the end of new.js.erb).

  23. Nicolas 9 March 2010 at 10:03 pm Permalink

    You can do something like

    repond_to |format| do
    if @myvar.save
    format.js ……
    else
    format.js { render :layout => false, :partial => “error” }
    end
    end

    I think it should work perfectly.

  24. Don Karlos 26 March 2010 at 1:32 pm Permalink

    Thank you for this great article! Your code is concise and very useful, and it works great. With your patterns it will be easier to convert to new Rails 3 conventions. Unobtrusive JavaScript FTW!

    Again, thanks a lot! :)

  25. Gerrit 22 April 2010 at 10:40 am Permalink

    Hi Nicolas,

    at first big thanks for your post. I read that with Rails 3 unobtrusive jQuery helpers will be supported and i would like to know if your basic ajax-application can get improved from this new features in Rails 3?

    Greets, Gerrit

  26. Chris Whamond 18 June 2010 at 9:20 am Permalink

    Thanks very much for taking the time to put this together, Nicholas. I’ve been making the switch from Prototype and your tut helped me understand jQuery Ajax a lot better. Works great with jQuery UI, too!

  27. Chris Whamond 20 June 2010 at 2:56 am Permalink

    Nicholas:

    I had a follow-up question:

    I’m implementing this as a nested resource in a Post model. Post has_many comments.

    My old (Prototype) form read:

    post_comments_path(@post), :html => { :id => ‘comment_form’} do |f| %>

    My comments controller create action:

    def create
    @comment = current_user.comments.build params[:comment]
    @comment.app_id = params[:app_id]
    respond_to do |format|
    format.html { redirect_to comments_path() }
    format.js {render :layout => false}
    end
    end

    I tried the post_comments_path(@post) and it doesn’t work.

    Also, the Cancel button in the _form partial takes you back to the comments_path(). In other words, the comments index. How would I modify that so it stays on the same post “show” page? I would have thought the syntax would be post_comments_path(@post). A little help on syntax for nested resources would be great! Thanks again!


Leave a Reply