Ruby on Rails - Ajax

AJAX and Javascript in Rails

There is a lot that needs to be learned to use ajax with rails. You may read that rails makes ajax easy, don't believe it. You have to climb a bunch of learning curves before you can even tackle the rails style UJS (unobtrusive javascript) and others rails specific issues. You will want to be fluent with css and javascript of course. You had also better get familiar with jQuery. You can bypass the insanity of Coffeescript without much trouble. All of this is compounded by the terrible or nonexistent rails documentation.

Let's dive in!

Firebug

Firebug can be extremely valuable for debugging Javascript/Ajax. I was bewildered for a time when my javascript files didn't seem to be loading, the Firebug console immediately showed me the syntax errors that were causing the trouble.

Coffeescript

I tried coffeescript, and words do not suffice to describe how much I hated it. You can use rails without being crippled by coffeescript, and it is not even much trouble to do so, so take heart:

CSS and javascript

Before diving into Ajax, you really need to be fluent with HTML, CSS, and javascript. I am always forgetting the basics of CSS, so here are some quick reminders.

You can give almost any (maybe any) HTML element an "id" or a "class". An id applies to only one element, and you use the following to apply style to it:

#dog {
    color:red;
}
A class applies to all elements with that class, you use this:
.dog {
    color:red;
}
You can restrict this to certain types of elements like so:
a.dog {
    color:red;
}

JQuery

Rails also is currently using the JQuery javascript library. I am sold on this -- you really do want to use some javascript library to abstract away any browser specific issues. (let the JQuery library writers sweat over that ugly nonsense). Note that JQuery is a client side library.

I say they "are currently using" because I know the trends in the rails community to jump from one bandwagon to another (Prototype was the javascript "library of choice" not all that long ago amongst the rails crowd). My 5 year old book (published in 2008), "the Rails Way", boldly proclaims that it is up to date for Rails 2.0, and is now wildly out of date. Sometime in the not too distant future we will be on to something else and all of this will be ridiculously irrelevant and out of date too.

There is all kinds on unclear and worthless documentation on Jquery, here are some vital things to know:

document.getElementbyId("x")   -- becomes --> $("x")
document.getElementbyTagName("img")   -- becomes --> $("img")
document.getElementbyClass("x")   -- becomes --> $(".x")

It is very common to specify an anonymous function for an event:

$("#mine").click(  function () { body; });

jQuery and Ajax

JQuery gives you 4 methods to do ajax: The load() call is a streamlined version of get() -- or in some cases post(). The get() and load() calls are streamlined versions of ajax(). The ajax() call is fully general with all the options.

jQuery load()

Suppose in your page you had an element like this:

To insert a bunch of static html (or whatever) in its place,
you would just do:

$('#youradhere').load('yourad.html');
What I actually did in my mounts.js.coffeescript file was this:
$ ->
    $("#find_location").click ->
           $("#loadtest").load("/load.html")
This grabbed public/load.html and injected it into a div with id "loadtest". Clicking the button repeatedly just replaces the same content. Nice. Note that the load() call can send extra stuff to the server, and can invoke a function when it finishes to do error recovery and such. It has some other tricks too.

The jQuery get() function can take a data argument that has stuff it is supposed to pass to the server like so:

$.get("test.php", { name:"Wally", rank:"Private" });

jQuery get() and post() with html

These are shortcuts for the more general jQuery.ajax() call and have essentially the same interface.

I bind to the same button click used in the load() example above using the following. Note that while this is coffeescript, I wrap a snippet of javascript in backticks to force it down coffeescripts throat. I prefer curly brackets to the insane tab nonsense that coffescript offers.

$ ->
        $("#find_location").click ->
                info = $("#mount_location").val()
                `$.get("/find_loc", { info: info }, function(data) {
                    $('#loadtest').html(data);
                });`
On the server side, I am now bouncing through a rails route and into my controller where I have the following code:
  def find_loc
    @loc_info = params[:info]
    #  renders app/views/mounts/find_loc.html.erb
    respond_to do |format|
        format.html { render layout: false }
        format.js
    end

  end
By default $.get asks for html. I suppress the layout that all my normal views get (we don't want a layout wrapped around an ajax response). Then the file find_loc.html.erb gets returned more or less verbatim to the browser, and the code there slaps it into the waiting div with id="loadtest".

jQuery get() and post() with json

By adding an additional dataType argument to the jQuery.get() call, the flavor of data expected to be returned can be specified. The following example specifies "json". A person could specify "xml" if he was stupid or enjoyed suffering. It is not possible to specify "js".
$ ->
        $("#find_location").click ->
                info = $("#mount_location").val()
                `$.get("/find_loc", { info: info }, function(data) {
                    $('body').append("

" + data.name); $("#loadtest").html(data.info) }, "json" );`

I took a long bit of experimenting to find out that when this is requested, the "data" returned is actually a javascript object, not some kind of string. Very handy, but not documented that I could find. To work with this, the server side was changed to:
def find_loc
    @loc_info = params[:info]

    # concocted ruby hash for response
    reply = { name: "micros", info: @loc_info }

    respond_to do |format|
        format.html { render layout: false }
        format.js { render json: reply }
    end
end
The "render json: reply" instructs that the ruby hash be converted to json to be sent to the client. Apparently jQuery on the client converts this transparently to a javascript object. A lot of handy plumbing.

Everything still works if I change format.js to format.json -- so I am unclear on why these both exist (or why format.js exists at all given we are talking entirely about json). It is equally unclear what purpose is served by find_loc.js.erb.

We can change this to an array instead of a hash without any trouble:

$ ->
        $("#find_location").click ->
                info = $("#mount_location").val()
                `$.get("/find_loc", { info: info }, function(data) {
                    var rsp = '';
                    for (var i = 0; i < data.length; i++) {
                        rsp = rsp + "

" + data[i]; } $("#loadtest").html(rsp); }, "json" );`

And on the server side:
def find_loc
    @loc_info = params[:info]

    reply = [ "micros", "dog", "cat", @loc_info ]

    respond_to do |format|
        format.html { render layout: false }
        format.json { render json: reply }
    end
  end

Unobtrusive Javascript

Rather than setting up lots of onclick events, the "rails way" is to use what they call "unobtrusive javascript". Consider the following:

<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
This explicitly calls inline javascript (and could call a javascript function if it wanted to). The trick is to do the following instead:
<a href="#" data-background-color="#990000">Paint it red</a>
It is possible to set up a "watcher" that takes action when it sees something going on with a attribute that looks like "data-*", the following is suggested, which requires more knowledge of coffeescript and jQuery than I possess so far:
paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor
 
$ ->
  $("a[data-background-color]").click ->
    backgroundColor = $(this).data("background-color")
    textColor = $(this).data("text-color")
    paintIt(this, backgroundColor, textColor)

That explains the idea, but the devil is in the details. Rails will do a lot of this for you, but the details of the plumbing are essentially undocumented. You can grub around and piece things together. If you get lucky, rails will do all the dirty work for you and you just need to insert special magic custom attributes in your html:

  • data-remote --- boolean, if true, submit via AJAX.
  • data-method --- the method to use in form submissions.
  • data-disable-with --- disables form elements during a form submission
  • data-confirm --- confirmation message to use before performing some action.

On the server side, you do something like this:

def create
  @user = User.new(params[:user])
 
  respond_to do |format|
    if @user.save
      format.html { redirect_to @user, notice: 'User was successfully created.' }
      format.js   {}
      format.json { render json: @user, status: :created, location: @user }
    end
  end
end
"respond_to" is a controller helper (there is also respond_with). The magic here is that the ajax request includes a mime type that indicates what flavor of response should be returned. Based on this mime type, only one of the three "format" lines gets executed.

Here in the case of format.js the response is generated by rendering the file (for a users controller) app/views/users/create.js.erb.


Feedback? Questions? Drop me a line!

Ruby on Rails notes / [email protected]