How I learnt about jQuery Deffered thanks to Rails

If you are not a regular JS developer but Ruby one or whatever, you probably don’t know every feature of JavaScript or jQuery, even if you use them every day. But some of them are worth spending some time to get to know – for me one of such features was jQuery.Deffered which I would like to introduce in this post.

The context

I love the idea behind unobtrusive javascript, silently promoted with the release of Rails 3. It helps to keep the code clean, separate concerns of data and logic like controllers and views.

In one of our projects I had to replace the standard browser confirmation prompt with a  fancy one and I was wondering if I can just re-use the Rails unobtrusive solution with data-confirm param. Then, the problem occured.

The problem

Rails wisely assigns built-in confirm method to the $.rails.confirm, which allows developer to simply replace it with anything (s)he wants. The problem is, that you cannot provide any method which would open up a prompt, wait for click and return the boolean value as confirm does. No wait here, sorry. The problem is well known, but there is no simple solution yet.

O’rly?

Eight months ago (sic!) Mr akzhan proposed the solution – he rewrote jquery_ujs confirm to work with $.Deferred. After polishing, his work landed couple of days ago in assync-confirm branch of jquery-ujs maintainer repo. Unfortunatelly, Steve will not merge it with master untill it is used by more people at production. However, if you like, you can just download the fork, put it somewhere into your assets and require the standard jquery_ujs instead.

Here’s a simple code which works for me (with jConfirm and coffee-script):

$.rails.confirm = (message) ->
  answer = $.Deferred()
  jConfirm message, 'Please confirm', (result) ->
    if result
      answer.resolve()
    else
      answer.reject()
  return answer.promise()

And that’s all! Dead simple, isn’t it? It will work with standard data-confirm, e.g.:

link_to some_path,
  { :method => 'delete', :remote => true, :confirm => "o'rly?" }

How it works?

So, you can ask – what is this magic $.Deffered? In the simplest words it’s an object with stack of callbacks inside. You can add a new one from the outside, and resolve/reject the object which would end up with specific callbacks run. There are three stacks – one is called when an object receives resolve() call, another one when it receives reject() call, and lastly a one when it receives any of the two calls.

In the example above we’re creating a deffered object which would get resolve/reject call from the jConfirm callback, depending on user decision. Without waiting for response, the $.rails.confirm will return the misterious promise object – it’s a deffered object without resolve/reject methods. You can still add new callbacks, but you cannot decide when to run them. We left that decision to jConfirm callback.

It’s also worth knowing that you cannot set a state of deffered object twice. However if you add a callback after resolving/rejecting, it would be fired up instantly, depending on the state of course.

Conclusion

Read the whole doc at jquery.com and just use it when you need the ability to postpone adding your callbacks. There are some objects already deffered – like $.ajax. You don’t need to specify callbacks inside constructor, you can move the object around and add callback anytime you need with then, done, fail or always.

Disclaimer

If you decide to use assync-confirm version of jquery-ujs, make sure that a little bug is fixed, or fix it yourself.

Chosen: multiple choice select inputs made easy

Link

http://harvesthq.github.com/chosen/

Great tool for enhancing selects, multiple selects, grouped selects etc. made by the Harvest team (great timetracking tool we’re using on a daily basis).
We’re all about using native controls as much as possible, but multiple select boxes were always a non-usable pain in the ass. This one is a great solution in tight areas where you can’t afford displaying a list with checkboxes.

 

 

 

Rails, Ajax and jQuery

The more ajaxified application, the more fun it is to use. But it is also more painful do develop. What is written below is my approach to pairing Rails and Ajax. It’s a mix of tips I found over the net on blogs and forums. I use jQuery for JavaScript, but I don’t use jRails or any JS/Ajax helper methods provided by Rails. Note that all Javascript/HTML code presented here can be used even if you dont use Rails or Ruby as your web development platform. Let’s begin.

Rails is RESTful

Thanks to Rails’ RESTfulness the only thing to take care of server side is setting proper response in controllers’ actons.

class PostsController < ActionController::Base
  def index
    @posts = Post.find :all
    respond_to do |format|
      format.html
      format.xml { render :xml => @posts.to_xml }
    end
  end
end

Rails decides which format block to call basing on routes defined in routes.rb file (map.connect ‘:controller/:action/:id.:format’) and accept headers sent with request by the client.

In most cases we want Ajax requests to trigger format.xml blocks in our controllers’ actions, so we need to set proper accept headers. Let’s do it just once with application-wide setting.

// All ajax requests will trigger the format.xml block
// of +respond_to do |format|+ declarations
$.ajaxSetup({
  'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/xml")}
});

Browsers’ quirks

There is something worth noting here, a problem I had once with IE and Safari. The code above may work differently in various browsers. Browser set text/html accept header by default. Here IE and Safari will append text/xml to it so you’ll get something like ‘text/html; text/xml’, while Firefox will replace text/html with text/xml and you’ll get ‘text/xml’ only. This is very important because Rails will take the first format it detects in accept header and trigger respective block in controller’s action, which will be html for IE and Safari. Here’s a fix for this that shifts application/xml (if it is present) to the beggining of accept headers array.

class ApplicationController &lt; ActionController::Base
  before_filter :correct_safari_and_ie_accept_headers
 
  def correct_safari_and_ie_accept_headers
    request.accepts.sort!{ |x, y| y.to_s == 'application/xml' ? 1 : -1 } if request.xhr?
  end
end

Ajaxify your links

Here’s a quick way to ajaxify your existing links. Add this JavaScript to your application.js file.

jQuery(document).ready(function() {
  // All A tags with class 'get', 'post', 'put' or 'delete' will perform an ajax call
  jQuery('a.get').livequery('click', function() {
    var link = jQuery(this);
    $.get(link.attr('href'), function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.post').livequery('click', function() {
    var link = jQuery(this);
    $.post(jQuery(this).attr('href'), "_method=post", function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.put').livequery('click', function() {
    var link = jQuery(this);
    $.post(jQuery(this).attr('href'), "_method=put", function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.delete').livequery('click', function() {
    var link = jQuery(this);
    $.post(jQuery(this).attr('href'), "_method=delete", function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.get, a.post, a.put, a.delete').removeAttr('onclick');
});

Just add a CSS class .get, .post, .delete, or .put to a link to make turn it into an ajax-link. I recommend you use LiveQuery plugin which will automatically bind click events to new links that appear on the page (loaded with Ajax call for-example). You can optionally set ajaxtarget attibute of the link. It expects a selector of a container in which you want to place the response.

link_to 'my cool article', article_path(@article), :class => 'get', :ajaxtarget => '#article_container'

Ajaxify your forms

For this you’d need jQuery Form Plugin.

  jQuery('form.ajax').livequery('submit', function() {
    jQuery(this).ajaxSubmit();
    return false;
  });

Now all your forms that have “ajax” class will be submitted via Ajax.

<form class="ajax">
  ...
</form>

CSRF and authenticity token

Rails has built-in protection from cross-site request forgery attacks. It relies on an authenticity token which Rails look for when dealing with POST, PUT or DELETE requests, so this token needs to be sent by the browser together with the request. The token is automatically added as a hidden field to any form you create with form_for method, it is also attached to links that have :method param set to :post, :put or :delete. In fact the token is added dynamically by Javascript code placed in link’s onclick attribute. However in one of code snippets above we stripped that onclick attribute from links to prevent the page reload after we click the link. Now we need to attack that token ourselves. First we will alter our application layout:

<head>
  <% 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 %>
</head>

Now we need to ensure that the token is sent together with ajax requests.

jQuery(document).ready(function() {
  // All non-GET requests will add the authenticity token
  // if not already present in the data packet
  jQuery("body").bind("ajaxSend", function(elm, xhr, s) {
    if (s.type == "GET") return;
    if (s.data && s.data.match(new RegExp("\\b" + window._auth_token_name + "="))) return;
    if (s.data) {
      s.data = s.data + "&";
    } else {
      s.data = "";
      // if there was no data, $ didn't set the content-type
      xhr.setRequestHeader("Content-Type", s.contentType);
    }
    s.data = s.data + encodeURIComponent(window._auth_token_name)
                    + "=" + encodeURIComponent(window._auth_token);
  });
});

We’re done, we have our ajax requests protected from CSRF attacks.

Modifing page after Ajax calls

Standard way to do page modification after Ajax call is to use Javascript code that inserts content returned by the call somewhere on the page. The other method is to put the modifying code in views that are returned by the server and just execute it in the browser. For this I’d recommend another jQuery plugin – Taconite. As the author says: “The jQuery Taconite Plugin allows you to easily make multiple DOM updates using the results of a single AJAX call. It processes an XML command document that contain instructions for updating the DOM”. Thanks to this you can for example easily use flash messages in your Ajax views.

Let this be a part of your usual layout:

<div id="flash_notice" class="flash"<%= ' style="display: none"' unless flash[:notice] %>><%= flash[:notice]  %></div>

Now let this be your taconite layout you’d use when returning views for Ajax requests:

<taconite>
  <hide select="#flash_notice" />
  <% if flash[:notice] %>
    <replaceContent select="#flash_notice">
      <%= flash[:notice] %>
    </replaceContent>
    <fadeIn select="#flash_notice" arg1="slow" />
  <% end %>
  <%= yield %>
</taconite>

This will display flash notice messages with fade-in effect after Ajax requests. Similarly you can update other elements of the page.

What’s in your toolbox?

I would love to hear from you on how you deal with Ajax in your web applications. What libraries/plugins do you use?