Drag 'n Drop sortable lists are a great way to provide a UI for sorting, well, lists of things. Most Rails examples outinthewild use prototype/scriptaculous and the built in Rails javascript helpers. In this walkthrough we'll provide the same functionality using JQuery instead. We will not be using the built in Rails javascript helper. Instead we'll write Unobstrusive Javascript using JQuery.
For this example we'll use a UserStory and a Task model:
The very first step is to download JQuery into your Rails app, as well as some basic setup that will make our lives easier when dealing with Unobstrusive Javascript (UJS). UJS is overlooked amongst web app developers. It's all about separation of concerns. Remember in the late 80s and 90s when it was common to throw style right there on your HTML with things like color="magenta" or tags like <center>? Same thing is happening with Javascript: separate behavior from content and presentation. So instead of saying <ahref="#"onclick="some_function()">foo</a>, you want to create a plain old link, and unobstrusively change the click's behavior via javascript. The basic feel for this looks like:
<!-- in your view --><aid="foo"href="http://foo.info">This is foo!</a>
//in application.js (possibly)$(document).ready(function(){
$('#foo').click(function(){
//handle the clickreturnfalse; //cancel the browser's traditional event.
});
});
Just like we use CSS selectors to style an element, we can use JQuery selectors to describe the behavior of the element. Just like you may have styles.css, you may have application.js. This separation of behavior helps avoid cross browser inconsistencies, dry up and reuse your code, as well as provide graceful degradation to user agents that don't even support Javascript. For instance, a form may be submitted via AJAX if the browser supports Javascript, or the traditional action may execute if it doesn't. I've come to the point where looking at something like <ahref="#"onclick='foo()'>foo</a> has the same effect as seeing style code in the middle of an HTML document: repugnance.
Set yourself up for JQuery
Head over to the JQuery site and download the minified (production) version of JQuery. The sortable() function is part of JQuery UI, which you can download from here. Place both under public/javascripts and include it on your layouts. Pretty standard stuff.
Add a content block on your layout for :javascript. This pattern was picked up while hacking on the bostonrb site'scode and I've stolen it for my own projects. It is a great way to throw UJS in your views. This should be at the bottom of your layout, right before the closing
tag (or in it's own partial along with other javascript related stuffs):
Having that out of the way, we need to set up jQuery's AJAX requests. We can use JQuery's $.ajaxSetup() hook to set the appropriate headers. Additionally, we'll include Rails' authenticity token on our AJAX Post requests.
For this to work, we need to store the Rails authenticity token somewhere. One option is to simply store it on a javascript variable as described here. Add this to your layout:
<%= javascript_tag "var AUTH_TOKEN =#{form_authenticity_token.inspect};" if protect_against_forgery? %>
Then, throw the following on your public/javascripts/application.js file - I haven't seen any downside to this, but leave a comment if you do.
//public/javascripts/application.js// This sets up the proper header for rails to understand the request type,// and therefore properly respond to js requests (via respond_to block, for example)$.ajaxSetup({
'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")}
})
$(document).ready(function() {
// UJS authenticity token fix: add the authenticy_token parameter// expected by any Rails POST request.$(document).ajaxSend(function(event, request, settings) {
// do nothing if this is a GET request. Rails doesn't need// the authenticity token, and IE converts the request method// to POST, just because - with love from redmond.if (settings.type == 'GET') return;
if (typeof(AUTH_TOKEN) == "undefined") return;
settings.data = settings.data || "";
settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN);
});
});
Having done that, let's prep our models. Let's assume you've created the UserStory and Task classes with the required has_many and belongs_to associations.
Models set up.
The models require just a few things: the acts_as_list plugin and a position attribute on the tasks table
Add acts_as_list :scope => :user_story to the Task model.
Optionally, add default_scope => 'position' to the Task model.
View setup
The idea is to create an <ul> of tasks that belong to a given user story. Each <li> contains a task and we have to "stage" the element's IDs so that when we serialize the list using JQuery, the task's IDs are sent over to the server via an AJAX request.
We also create a span with a class of "handle", which is where the user can hold on to when dragging and dropping tasks around.
<ulid="tasks-list"><% @user_story.tasks.each do |t| %><liid="task_<%= t.id -%>"><spanclass="name"><%= t.name -%></span><spanclass="handle">[handle]<span></li><% end %><ul>
(Unobstrusive) javascript setup
Now we are ready to wire in the javascript on your view. At the bottom of your view, and using the :javascript content block created earlier, use JQuery's sortable() function and attach it to the #tasks-list element:
This is basically saying: Take the element with an ID of #tasks-list and make it sortable. Do an HTTPPOST to the user_stories/prioritize_tasks path with the serialized tasks-lists as data. Note that we're also appending the user_story id as a post parameter so that our controller action knows which tasks to prioritize.
Feel free to go over the sortable() documentation to tweak the options.
Controller set up
The controller's work is to take the parameters sent in from the view and to set the position attribute of each of the user story's tasks. The parameters received in the controller look something like:
As expected, we receive the Rails authenticity token, as well as the id (the UserStory ID) and an array of task IDs (params['task'] contains task IDs in the order specified by the user).
Here's the implementation of the prioritize_tasks action:
This pretty much wraps it up! This process will become easier as Rails core evolves and the Javascript framework becomes easier to swap out. It is known that Rails 3's helpers will not produce inline javascript. Instead, they will add hooks to your DOM elements in the form of HTML5's custom data attributes which then can be used by unobstrusive javascript code to add the appropriate behavior to the DOM elements. Even though Rails will continue to ship with the Prototype/Scriptaculous frameworks by default, JQuery will be easier to plug in, and the Rails helpers that simply add data attributes to your DOM elements can be used to achieve all sorts of presentation behavior.
Regardless of that, the closer you are to your Javascript, the better you'll understand how the pieces play together and the more control you'll have over the app's client side behavior.