#147 Sortable Lists
- Download:
- source codeProject Files in Zip (106 KB)
- mp4Full Size H.264 Video (12 MB)
- m4vSmaller H.264 Video (8.34 MB)
- webmFull Size VP8 Video (20.2 MB)
- ogvFull Size Theora Video (14.6 MB)
The site we’re working on has a page that shows a list of Frequently Asked Questions. The site administrators would like to be able to sort the list so that the FAQs can be seen in a specific order by end users instead of in the order they were created. We’ll implement a drag and drop interface that will post the updated order back via an AJAX call to enable them to do this.
Changing The FAQ Model
The Faq model in our application has two fields: question
and answer
. The first thing we need to do is to add a position attribute so that we can give each FAQ a position in the list by which it can be sorted. To do that we’ll generate a migration.
script/generate migration add_position_to_faqs position:integer
The name of the migration and the argument we’ve passed to it are enough for it to generate all of the migration code, so we can now just run rake db:migrate
. (For more information on this see the migrations page on the excellent Rails Guides website.)
Updating The View Code
Next we need to add the prototype and scriptaculous libraries to the application. To do this we just add the following line to the head section of our layout file.
<%= javascript_include_tag :defaults %>
Having done that we’ll now need to make some changes to our index
view file. The FAQs are displayed as an unordered list; in order to be able to identify each one we’ll have to give it a unique id. We’ll give the list itself an id too. To do that we’re going to use the content_tag_for
method, which will create an HTML element, in this case an <li>
, with an id attribute based on the model’s numeric id. This is how the updated index view looks.
<h1>FAQs</h1> <ul id="faqs"> <% @faqs.each do |faq| %> <% content_tag_for :li, faq do %> <%= link_to faq.question, faq_path(faq) %> <% end %> <% end %> </ul> <%= link_to "New FAQ", new_faq_path %>
The index view with ids for each list element.
If we refresh the page and look at the source we can see that each li element in the list now has an id of the form faq_n
, where n is the model’s id.
Making The List Sortable
All we have to do to make the list sortable is add the sortable_element
helper method to the index view. We’ll pass it the id of the element we want to be sortable, and a URL that is called via an AJAX request so that the updated positions can be stored in the database. As we don’t yet have a method for storing the updated positions we’ll leave the URL for now and come back to it later.
<%= sortable_element('faqs', :url => 'TODO') %>
The items in the list can now be dragged and dropped, but the new order isn’t persisted back to the database. When the page is reloaded the items are shown back in their default position. We are going to have to write a method in our FaqsController
that will store the updated position orders. The controller currently has the standard seven RESTful actions for listing, showing, creating and deleting FAQs; we’ll add a new method called sort
.
def sort params[:faqs].each_with_index do |id, index| Faq.update_all([’position=?’, index+1], [’id=?’, id]) end render :nothing => true end
The sort
method will loop through each FAQ parameter passed to it and update the position for that FAQ. The FAQ’s ids are passed in the correct updated order so we use update_all
to set the position attribute of each FAQ to be its index in the list plus one.We don’t need to send anything back from the AJAX call so the method finally returns nothing.
Now that the positions are being stored we’ll make a small change to our index action to tell it to get the list of FAQs ordered by position.
def index @faqs = Faq.all(:order => ’position’) end
There’s one further change to make, which is to the routes file. Our sort
action isn’t one of the seven default actions so we’ll have to add it.
map.resources :faqs, :collection => { :sort => :post}
The line above adds the new action and makes it a POST request, which is the type that our AJAX call uses when making XMLHTTP requests. Now that we have made these changes we can go back and add the URL to the sortable_element
method call that we created earlier.
<%= sortable_element(’faqs’), :url => sort_faqs_path %>
The sort_faqs_path
will call our sort method in the FaqsController
via AJAX. Now when we drag items about and then reload the page the items new position is persisted to the database.
Improving The UI
The list of FAQs is now fully functional, but the user interface is still a little rough around the edges. For example if we use one of the links to drag an item around then the link will be triggered and we’ll be taken to the page for that item. We can improve the interface by adding a handle to each item that will become the draggable area. In the view code we’ll add a <span>
element to each item in the list.
<% content_tag_for :li, faq do %> <span class="handle">[drag]</span> <%= link_to faq.question, faq_path(faq) %> <% end %>
The span has a class of handle
, so that we can tell the sortable_element
method which element within each item should be draggable. This is done by adding a handle option to the method.
<%= sortable_element(’faqs’), :url => sort_faqs_path, :handle => ’handle’ %>
To make the draggable area more obvious we’ll add some CSS for the handle so that when the cursor is over it it changes to a ’move’ cursor.
li .handle { color: #777; cursor: move; font-size: 12px; }
Dragging Using The Handle
Using acts_as_list
While it’s not required for our application, we’re going to use the acts_as_list
plugin to supply some more functionality. We’ll download it from github and install it in the usual way.
script/plugin install git://github.com/rails/acts_as_list.git
To use it we’ll add it to our Faq
model.
class Faq < ActiveRecord::Base acts_as_list end
Now the Faq model will be treated as a sortable list. One of the advantages of using acts_as_list
is that when we create a new FAQ, the position column will automatically be filled for the new FAQ.
Our list is now fully draggable. If this was a real application the next part would be to restrict the dragging functionality to administrative users only, but that is out of the scope of this episode so we’ll stop here.