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
I understand that you can’t make it work with table rows (check the scriptalicious website in case I am misremembering). However, at least for firefox, using lists with display set to table, table-row, table-cell works great (gives you all the benefits of a table plus some).
In particular I did an outer ul with display: table (list-style none of course). Nothing special about the li elements in this list. Inside these li elements I have another ul element with display: table-row and the li elements inside this have display: table-cell. Be carefull with the sizes (widths) though. If they don’t match up right things can get really screwed up.
I think it is allowed for a browser to ignore these display values so I don’t know if it will work in other browsers.
—logicnazi
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
—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
—Dan
Dan, you’re right. It lets you drag one item then it just fails. Not good at all. Thanks Microsoft. :(
—TheoGB
&authenticity_token=<%= form_authenticity_token %>
form_authenticity_token will generate a valid token that rails needs to validate the request.