Ruby on Rails
HowToUseDragAndDropSorting

Controller

In your controller, get your list of items from the model like so:


def list
  @items = List.find(:all)
end

You’ll need something to update the positions of your list items upon sorting as well. I admit, this might be heavy handed – it updates the position of each item in the model – but I don’t know of any other way. This assumes you’re using the position column as well:


def update_positions
  params[:sortable_list].each_index do |i|
    item = ListItem.find(params[:sortable_list][i])
    item.position = i
    item.save
  end
  @list = List.find(:all, :order => 'position')	
  render :layout => false, :action => :list
end

View


<div id="items">
<ul id="sortable_list">
<% @items.each do |item| %>	
  <li id="item_<%= item.id %>"><%= item.value %></li>
<% end %>
</ul>
</div>

<%= sortable_element('sortable_list', :update => 'items', :url => {:action => :update_positions}) %>

The action shown in url will be executed when the item is dropped, which will then update the positions, and finally grab the newly rendered list and drop it into the items div.

Layout

In your layout, be sure that you have included the drag and drop script.


  <%= javascript_include_tag 'dragdrop.js' %>

Testing

If you want to write a test for the controller, you will need to simulate the request from the browser. This requires putting an array into the parameter hash, like this:


xhr :post, :update_positions, {:sortable_list => [3, 1, 4, 8]}

The 3, 1, 4, 8 are examples of the item.id values required for the test. Once in the controller, the parameter values are then:


params[:sortable_list][0] ... 3
params[:sortable_list][1] ... 1
params[:sortable_list][2] ... 4
params[:sortable_list][3] ... 8

—juga

Beware to remember item_ as the li id, using just a number doesn’t work.
—agenteo


It seems to me that the _update_positions_ method can be written more concisely as:


def update_positions
  params[:sortable_list].each_with_index do |id, position|
    ListItem.update(id, :position => position)
  end
  render :nothing => true
end

For most situations it is probably sufficient to leave the newly sorted list as it is without redrawing it with AJAX:


<%= sortable_element('sortable_list', :url => {:action => :update_positions}) %>

— eventualbuddha

But if you don’t redraw the list after each sort then successive sorts will keep using the same original positions, and not the newly sorted positions no?
—Chris

— No works fine for me.
— Tobie

_I can not get it to redraw without doing a page refresh…
— Mike


Normal behavior for _acts_as_list_ is to have the position column start at 1 (and not 0 as in eventualbuddha’s above solution — which works beautifully btw.). Proposed modification to eventualbuddha’s solution:


def update_positions
  params[:sortable_list].each_with_index do |id, position|
    ListItem.update(id, :position => position+1)
  end
  render :nothing => true
end

Incrementing position by 1 seems to do the trick.

— Tobie

In Rails 2.0, the former is not working for me. Instead I had to write:


  def sort
    params[:sortable_list].each_with_index do |id, pos|
      ListItem.find(id).update_attribute(:position, pos+1)
    end
    render :nothing => true
  end

Is anybody having the same problem?

— miguelsan


This works great with list tags; how can it work with table rows too ?

— Answer

You have to add the tag, and use it as the container. You can then specify :tag => ‘tr’ in the view helper.


With the controller, I found that I had to do this to get it to function correctly on refresh:


def list
  @items = List.find(
    :all,
    :order => 'position')
end

In your model, to avoid defining this in your controller, add the :order parameter to acts_as_list or acts_as_tree…
ie:


acts_as_tree :order => 'position'

— Question

The act as a tree option doesnt work for me. Maybe Im doing it wrong but I have to explicitly tell it to do it.

Any ideas for how to make this work with multiple arrays? Perhaps a way to drag and drop between the two? Assuming I was capable of converting one item to the other, how could it be done?

This will not work in Internet explorer.

— Answer

If you want to work with two arrays you need to do it with draggables and droppables. Go see the scriptalicious page.

Also I think sortables have some issues if you try and update the sortable list while the list is working. You may wish to stop sortables (with Sortable.dstroy) before updating the list and then restrart it.

—logicnazi


How could I implement drag and drop for a hierarchy (act_as_tree) ?

—Peter

Drag n Drop Ajaxified Tree

Implement the Drag n Drop hierarchy using acts_as_tree..
Hi peter… check out the source code for the tree here Ajaxified Tree



I have implemented the DnD for nested trees by placing the js call after each uniquely id list like so


<ul id="sortable-tree-1">
  <li>blah</li>
  <li>
    <ul id="sortable-tree-2">
      <li>foo</li>
      <li>bar</li>
    </ul>
    <%= sortable_element("sortable-tree-2", :url => {:action => :update_positions}) %>
  </li>
</ul>
<%= sortable_element("sortable-tree-1", :url => {:action => :update_positions}) %>

and then modifying the ‘update_positions’ action like so


def update_positions
  params.each_key {|key|
    if key.include?('sortable-tree')
      params[key].each_with_index do |id, position|
       ListItem.update(id, :position => position+1)
      end
      render :nothing => true
    end
  }
end

-felix

You can also use the in-built script.aculo.us tree support in recent versions:


<%= sortable_element 'list_for_root', :url=>"sort", :constraint=>false, :tree=>true %>

Then you need to do something a bit recursive in the controller to save the ordering. This is a really rough first working draft, so feel free to improve and overwrite this…


# handles AJAX responses from script.aculo.us drag/drop sortable tree
  def sort
    save_tree(params["list_for_root"]["0"], nil, 1)
    render :nothing=>true
  end
  
  # assumes that each set of nodes contains a key "id", and that the rest are ordered numeric keys
  def save_tree(treenodes, parentid, pos)
    node_id = treenodes["id"]
    pc = ProductCategory.find(node_id) #this is my model, obviously you'd use your own here
    pc.parent_id = parentid
    pc.position = pos
    pc.save
    pos += 1;    
    
    # if there are any nodes left after the ID is gone there are children, 
    # and we'll need some recursion...
    treenodes.delete("id")
    treenodes.sort.each { |child| pos = save_tree(child[1], node_id, pos) } if treenodes.length
    pos
  end

The ProductCategory model acts_as_tree, but this doesn’t really use any of its methods – but it does operate on the same default field names. For rendering the list I’m using DRYML, so I won’t provide code for that to avoid confusing people.

— Finn


sortable_element seems to break in IE7, has anyone else had this problem?

—Dan

Dan, you’re right. It lets you drag one item then it just fails. Not good at all. Thanks Microsoft. :(

— TheoGB


In rails 2.0, if you’re trying to submit some crazy AJAX you’ve coded manually and you’re getting an Invalid Authenticity Token error, be sure to add the following to the query string of parameters being submitted:



&authenticity_token=<%= form_authenticity_token %>

form_authenticity_token will generate a valid token that rails needs to validate the request.