jQuery + Rails ( Part 1 )
This article is not really about jRails but about unobtrusive JavaScript in Rails using jQuery.
So I decided to switch to jQuery for my Ruby on Rails apps. I’m quite enthusiast and the whole thing about this turnover is that I figured out that it makes sense. There is few talks about it, some have made the move. There is even a plugin that will make almost all your helpers you wrote in ERB works without you need to do anything about it. Many of your RJS code will still work. You can look at which of those features will still work out-of-the-box here.
I’m speaking about replacing completely Prototype and Scriptaculous by jQuery here, got it?
Why jQuery and Rails together makes sense?
- jQuery has tons of plugins, just like Ruby on Rails
- jQuery is completely unobtrusive (MVC is great, but for Web applications MVC + J is better)
- javascript with jQuery is fun, Ruby is fun, Rails is fun
- jQuery design philosophy rules
- The community is big and active
Tutorial: An unobtrusive ajax sortable list updated live
Prerequisites to follow my example
1) You might want ot create a new rails application. We will need Rails 2, in order to use the new “js.erb” template (those aren’t RJS, see further in this article)
Tips: Do not use the rails binary from rails installed as a gem if you plan to upgrade to edge. In that case use the edge binary:
$ ruby rails/railties/bin/rails foo
$ mv rails foo/vendor
$ cd foo
… and you’re on silly edge! :-)
2) Remove everything in public/javascripts except application.js and put the jQuery file there. Also we will u
se the Interface jQuery plugin. You can download only the UI components you need. Here we’ll need the Sortable modul
e. Put that downloaded file in your javascript folder too.
3) To simplify this example we will not protect ourself from forgery for now. I will give a solution to this in a further post. So in your app/controllers/application.rb, comment the following line
4) Update your config/database.yml if needed, create the development DB then start your server and jump into a first example.
We won’t need jRails for now. I could have install it and use the sortable helper that do exactly what the one from Scriptaculous do, but we will later need to
update a list via Ajax and for that reason, we will use the Interface plugin to keep it updated live as new contacts get add by an Ajax form (look further post).
Setup
Let the scaffold generator generates everything we need for our contact resource
$ rake db:migrate
Point out your browser to http://localhost:3000/contacts to see if you don’t have any errors. Add up as many contacts as you want for testing purpose.
You now must add the needed JavaScripts files. Add those into app/views/layouts/contacts.html.erb. It should almost looks like this.
<%= javascript_include_tag ‘interface.js’ %>
<%= javascript_include_tag ‘application.js’ %>
Now open app/controllers/contacts_controller.rb and update the code in the index action to sort the contacts by their position.
Create the list
Now we will create the list for the index template. Don’t remove the existing code so we can keep the links to our actions. Add the code just add the following at the end of the app/views/contacts/index.html.erb file:
<ul id="this_id_is_just_needed">
<% for contact in @contacts %>
<li id="contact_<%= contact.id %>" class="sortableitem">
<%= contact.email %></li>
<% end %></ul>
Note: The “ul” element just need an id. Otherwise, some of the sortable functions won’t work.
Making the list sortable
It’s time to add behavior to our plain html. We will do this in public/javascripts/application.js. That is what we call unobtrusive JavaScript because we didn’t use any helper in our index templat
e that generate javascript. jQuery is especially nice doing that because it makes it easy to select elements and apply behaviors on them. The following is the code I use to make it. I’ve DRY it out to make it e
asier to maintain. It is fully commented, I let you read and I’ll explain after.
$(document).ready(function () {
/*
* The global object of the application.
* It acts as a namespace to avoid collision
* with any other code
*/
if (typeof MYAPP == "undefined") {
var MYAPP = {};
}
/*
* This is our version of Interface Sortable.
*
* All Sortable options have predecedence
* over the default set here.
*
* We don’t have to care about Ajax here other than
* posting the serialized data to an url. Data have been
* already serialized by Sortable before calling "onchange".
*
* Ajax events should hook the post request before it is sent
* in order to set the correct header and datatype.
*
* @param {Array || Element}
* selector The css selector
* @param {funct} fn The Sortable function
* @param {object} opt The Sortable options
* @param {String} url The url to post the data
*
* It returns the selector to keep chainability
* and being consitent with the jQuery philosophy.
*/
MYAPP.sortable = function (selector, fn, opt, url) {
opt = $.extend({
onchange: function (changes) {
$.post(url, changes[0].hash);
},
opacity: 0.7,
fit: false,
accept: ’sortableitem’,
activeclass : ’sortableactive’,
hoverclass : ’sortablehover’,
helperclass : ’sortablehelper’
}, opt);
fn.call(selector, opt);
return selector;
};
/*
* Ajax event.
* Called before sending any XHR.
*
* @params {Event} evt The event
* @params {XHR} request The XmlHTTPRequest
* @params {object} settings Settings for the POST XHR
*/
$(document).ajaxSend(function (evt, request, settings) {
/*
* If we want to keep the Rails convention where XHR should
* respond to javascript request,
* then we must change the header accordingly.
*
* The default header is XML.
* Rails conventions for XML request are not for XHR.
*
*/
request.setRequestHeader("Accept", "text/javascript");
/*
* Expected server response can be one of the following values
* => ‘xml’, ’script’, ‘json’ or null
*
* null means that we will handle the response ourselves.
* It is the default option.
*
* Because the MVC convention in Rails,
* it is in the Views that behaviors are defined, not here.
* Though we cannot use ‘json’ nor ‘xml’, because the server
* response should have instructions
* and not contains only datas.
*
* The response will have to be evaluated as JavaScript.
*/
settings.dataType = null;
});
/*
* Ajax event.
* Called when the server response is successful
*
* The server response is plain JavaScript that must be evaluated
* because we set the dataType to null
*
* @params {Event} evt The event
* @params {XHR} request The XmlHTTPRequest
* @params {object} settings Settings for the POST XHR
*/
$(document).ajaxSuccess(function (evt, request, settings) {
eval(request.responseText);
/* The flash messages and code could be in the server response
* and/or it could be here in the callback too, but the latter
* is more generic.
*/
$("#flash-notice").text("Success");
});
/*
* Ajax event.
* Called when the server response isn’t successful.
*
* It’s up to you to decide how to manage this.
* In our case, the server error message is:
* ajaxOptions.responseText
*/
$(document).ajaxError(function (XMLHttpRequest, ajaxOptions, thrownError) {
});
/*
* This is where we make the list sortable.
* Sortable options will overide the defaults.
* We can chain that function call as we would do
* with the jQuery object.
*
* That line could have been something like
* $("ul").Sortable({ … })
* But I prefer that way, it’s more readable
*/
MYAPP.sortable($("ul"), $.fn.Sortable, {opacity: 0.5},
"/contacts/order");
});
Explications
You see that my functions are wrapped in the MYAPP global object. I always to that choosing a better name though. MYAPP.sortable (The last line of the previous code listing) is the function that will call the real jQuery.Sortable function. I’ve written it like that, so we don’t repeat some of the options, like onchange that won’t change. All of the options can be overwrite at the MYAPP. call within the arguments. Check out the official Interface documentation for those options.
sortable
Hooking Ajax event
The good thing is that we can hook some Ajax events like ajaxSuccess or ajaxSend to define the behavior of our unobtrusive Ajax application. We have define it once we are done for all our Rails Ajax application. It’s completely unobtrusive and DRY.
Defining the order action
In our call on line 136 we set the post url for our list. The action order isn’t yet define in our controller, so we must do it.
# POST /contacts/order.js
#
def order
# An array of ids of contact of their new order
@ids = params.values.select { |v| v.is_a?(Array) }.first
# To send to the view, to display the new order
@order = @ids.join(", ")
i = 1
@ids.each do |id|
# get the last longuest number ending a string
# and take it as the contact id
c = Contact.find(id.gsub(/^.*([0-9]+)$/, "\\1"))
if c.position != i
c.position = i;
c.save
end
i += 1;
end
respond_to do |format|
format.js
end
end
Building the server response with POJS + ERB
POJS stands for Plain Old JavaScript. I told you that we won’t use RJS template. We could if we’ve wanted, install the jRails plugin and
use the RJS-style template to generate the JavaScript, just as if it were RJS, because jRails provide the JavaScript helpers needed to replace Prototype / Scriptaculous code by jQuery code. But we won’t write R
JS-like code. Instead of writing ruby code in a “.js.rjs” file, we will write JavaScript with embedded Ruby in a “.js.erb” file. Since Rails 2.0, you can do it. Before Rails 2.0, the minus_mor plugin were needed to do so.
So create the file app/views/contacts/order.js.erb and put the server response code in it:
.after("<span>New order is: <%= @order %></span>")
.next()
.fadeIn("slow", function() {
var that = this;
setTimeout(function () {
$(that).hide("slow");
}, 4000);
});
We are done! This code just append the new ordered list ids in the order they have been received by the server. It create a new element right after the list with some show and hide fade effects.
I’m quite new to Rails and jQuery and I will appreciate feedback on this.