Saturday, July 14, 2012

ajax with jquery in rails

So I'm going back through the updated rails books in an effort to see what all has changed since I last poked at it (as it turns, quite a lot).  One of the big changes was the removal of RJS (rails javascript) and the move to jQuery.  Unfortunately, the new jQuery interface doesn't seem to be referenced in the doco I've got nor was it easy to get a straight answer from googling, checking the js console, or even good old alert() debugging.

The main malfunction is that jQuery needs to be told to wait until the DOM has finished rendering before it can start doing anything asynchronously.  If you attempt to fire off an asynchronous event (like an .ajax(), .get(), or .load() call) before the browser is ready, the bind for the callback will be missed and nothing will happen when the other end of the call answers.

Given the way RJS worked and without any extra guidance, you'd expect a setup like this to work with a
in your application layout and an <%= button_do 'Add to Cart', line_items_path( :product_id => :product, :remote => true ) %> in the index view.

app/controllers/line_items_controller.rb
  def create
    @cart = current_cart
    product = Product.find( params[:product_id] )
    @line_item = @cart.add_product( product.id )

    respond_to do |format|
      if @line_item.save
        format.html {redirect_to @line_item.cart }
        format.js
       else
  #[.... rest of function ....]

And then in app/views/line_items/create.js.erb you would expect something like this to work:
alert( "This alert will popup" );
$('#cart').load( "<%= escape_javascript( cart_path( @cart ) ) %>" );
alert( "But you will never, ever see this one." );

But, it doesn't. It just fails quietly leaving you to google for answers.  Even more confusing is the fact that non ajaxy stuff you put in the file see what's happening will work, but nothing after the async function call will.

The solution I found was to wrap the ajax stuff in a document ready function (again in app/views/line_items/create.js.erb):
alert( "You will see this alert popup.");
$(document).ready( function($) {
  $('#cart').load( "<%= escape_javascript( cart_path( @cart ) )%>" );
} );
alert( "And you'll see this one, too, after the cart div updates!!!" );

This forces jQuery to slow it's roll and wait for the DOM to finish its ponderous task before firing off the event.  At least, that's what I think is going on... javascript isn't my strong point.  But that explanation sounds better than "When I wave the dead chicken like this, the sky makes thunder."

I also found someone that had come up with a solution of binding click events for any button with a "add_to_cart" class to the ajax:success event and use that to update the cart div, but that seemed like a giant trap to maintain and was a nonobvious solution to the problem.

No comments: