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
<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
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.