#147 Sortable Lists (revised)
- Download:
- source codeProject Files in Zip (86.4 KB)
- mp4Full Size H.264 Video (18.3 MB)
- m4vSmaller H.264 Video (7.55 MB)
- webmFull Size VP8 Video (10.9 MB)
- ogvFull Size Theora Video (15.8 MB)
Below is a page from a website that shows a list of Frequently Asked Questions. We want to add the ability to reorder the items in the list by dragging and dropping them and to have the updated order persisted to the database without having to reload the page. In the past we’ve done this by using Prototype and Scriptaculous’s Sortable helper, but these are no longer included with Rails, so how do we achieve this in Rails 3.1?
Introducing jQuery UI
Instead of using Prototype we’ll use jQuery UI. This comes with a Sortable plugin that makes it easy to make a list sortable with drag and drop. The plugin is configurable with a large number of options that we can use to customize the behaviour to suit our application.
Adding jQuery UI to our Rails application is simple. We don’t even need to download anything; all we need to do is add a line to the application.js
file.
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery-ui //= require jquery_ujs //= require_tree .
Modifying The View
The template that lists the FAQs looks like this:
<h1>FAQs</h1> <ul> <% @faqs.each do |faq| %> <li> <%= link_to h(faq.question), faq %> </li> <% end %> </ul> <p><%= link_to "New FAQ", new_faq_path %></p>
We’ll need to give the list an id so that we can reference it from JavaScript. We’ll also need to give each item in the list an id so that we can identify it when we post the updated order back to the server when we move the items around. The items need to have ids in the format <model_name>_<model_id> and we can use a helper method called content_tag_for to generate these for us. After we’ve made these changes the template will look like this:
<h1>FAQs</h1> <ul id="faqs"> <% @faqs.each do |faq| %> <%= content_tag_for :li, faq do %> <%= link_to h(faq.question), faq %> <% end %> <% end %> </ul> <p><%= link_to "New FAQ", new_faq_path %></p>
Note that it’s important that the content_tag_for
block has an equals sign in its opening tag as it outputs content.
Adding Sortable to The List
Next we’ll add the script necessary to make the list sortable. All we need to do is get the list and call sortable()
on it. We’ll wrap this code in the jQuery
function so that it only runs when the page’s DOM has loaded.
jQuery ->; $('#faqs').sortable();
When we reload the page now we’ll be able to drag the items in the list around.
There are various options that we can pass to sortable. We’ll use the axis
option so that the list only sorts vertically and also add an update
callback which will be triggered when an item is dropped.
jQuery -> $('#faqs').sortable( axis: 'y' update: -> alert('updated!') );
When we drag an item now it will only move vertically and when we drop it the callback will be triggered and we’ll see an alert.
Storing The Updated Order
When an item is dropped we’ll need to send some information back to the server so that we can store the updated order. As our application currently stands it will change back to the default when we reload the page.
The Faq
model needs a position
field to store each item’s position. We’ll create a migration for this, then run rake db:migrate
to add the new field to the database.
$ rails g migration add_position_to_faqs position:integer
Next we’ll update the FaqsController
’s index
action so that it fetches the FAQs in the order of the position field.
def index @faqs = Faq.order("position") end
When the order of the FAQs changes we’ll need a new action to handle the data that’s sent back to the server. The FaqsController
has the standard seven RESTful actions but none of these do quite what we need. The update
action comes close but we need to update multiple items at once. We’ll add a sort
action to the controller but just have it render nothing
for now.
def sort render nothing: true end
We’ll need to add this custom action to the routes file, in the faqs
resource. The sort
action will work with a number of Faq
s so we use collection
and post
for sort
. It might make more sense to use put
here but post
is a lot more convenient.
Faqapp::Application.routes.draw do root to: 'faqs#index' resources :faqs do collection { post :sort } end end
Back in the CoffeeScript file we can now update the code in the callback function so that instead of showing an alert
it makes a POST request to our new sort
action. It’s best not to put URLs directly into the CoffeeScript so we’ll add the callback URL as a new data
attribute in the list.
<ul id="faqs" data-update-url="<%= sort_faqs_url %>">
We can fetch this URL in our CoffeeScript code and use it to send the updated position information back to the sort
action. We can fetch this information by calling $(this).sortable('serialize')
. This will wrap up all the items in a format that we can send back to the server.
jQuery -> $('#faqs').sortable( axis: 'y' update: -> $.post($(this).data('update-url'), $(this).sortable('serialize')) )
When we reload the page now and change the position of an item it will trigger an AJAX call to the sort
action. When we take a look at the development log we’ll see how the parameters are passed in as a faq
parameter with an array of faq
id
s. These are sent in the order that the updated list is in.
Started POST "/faqs/sort" for 127.0.0.1 at 2011-10-17 22:05:47 +0100 Processing by FaqsController#sort as */* Parameters: {"faq"=>["1", "2", "5", "3", "4", "6"]} Rendered text template (0.0ms) Completed 200 OK in 21ms (Views: 20.2ms | ActiveRecord: 0.0ms)
In the FaqsController
’s sort
action we can now read those parameters and update each Faq
’s position by looping through each of them with its index
.
def sort params[:faq].each_with_index do |id, index| Faq.update_all({position: index+1}, {id: id}) end render nothing: true end
We use update_all
here so that all of the updates are made in a single query and set each Faq
’s position to be the index + 1
(as the index is zero-based) where its id
is the id from the parameter. When we change a FAQs position in the list now and reload the page the item will stay in that position.
Adding Handles
There’s currently no visual clue that the items on the page are draggable. To improve this we can add a handle to each item that tells the users that the items can be dragged. We’ll add a handle to each item in the template and give it a class
so that we can refer to it in our CoffeeScript.
<ul id="faqs" data-update-url="<%= sort_faqs_url %>"> <% @faqs.each do |faq| %> <%= content_tag_for :li, faq do %> <span class="handle">[drag]</span> <%= link_to h(faq.question), faq %> <% end %> <% end %> </ul>
handle
option in the sortable
function.
jQuery -> $('#faqs').sortable( axis: 'y' handle: '.handle' update: -> $.post($(this).data('update-url'), $(this).sortable('serialize')) )
We’ll add some styling to the handles, too, giving them a move
cursor as an indication that they can be dragged.
Adding a New Item
If we add a new Frequently Asked Question it will have a null
position
attribute. It would be better if it the new item was automatically given a position
with a value that would place it at the bottom of the list.
We can do this by using the acts_at_list
gem. This gem has been around for a while but it’s being actively maintained by Swanand Pagnis so it’s fine to use. As usual, we add this gem to our application by adding it to the Gemfile
and then running bundle
to install it.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not" required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'acts_as_list'
Once we’ve installed acts_as_list
we just need to add it to our Faq
model.
class Faq < ActiveRecord::Base acts_as_list end
The position
attribute of any new Faq
s we create now will be automatically set.