IdolHands.com :: Days in the Life of an Alpha Geek
If you're using Chris Bailey's Better Nested Set (or better yet, Brandon Keeper's awesome_nested_set) for your Ruby on Rails models, you may one day want to implement a drag-and-drop interface to reorganize your sets. Here's how I was able to implement it.
Let's take a dead simple example: a Category model that supports n levels of subcategories using better_nested_set.
>> Category
=> Category(id: integer, name: string, parent_id: integer, lft: integer, rgt: integer,
depth: integer, created_at: datetime, updated_at: datetime)
parent_id, lft, and rgt are all standard attributes required for better_nested_set. There's one extra attribute in there, though: depth.
I've found that in real-world applications, getting the level of a node in a nested set can be a very expensive operation (to the tune of 100ms), so I typically cache this value in the database and update it on save. Note that when you use better_nested_sets move methods, e.g. move_to_child_of, the plugin is making direct SQL calls to avoid object instantiation, so you'll need to manually call your update_depth method as well. This is annoying and easy to forget, but I think it's worth it from a performance perspective; I'd rather optimize for fast read operations than write operations.
So our category.rb looks like this:
class Category < ActiveRecord::Base
# Behaviours
acts_as_nested_set
# Callbacks
after_save :update_depth
# Validations
validates_presence_of :name
validates_uniqueness_of :name
def update_depth
unless self.level == self.depth
self.update_attribute(:depth, self.level)
self.children.each{|child| child.update_depth }
end
end
end
Simple enough so far.
In our view, we want to do a couple of things: display the nested categories with indentation, and allow the user to drag and drop between categories. This makes our view code a little complex.
Our index.html.erb file is pretty sparse:
<h1>Categories</h1>
<%- if @categories.empty? -%>
<p><em>No categories have been created yet.</em></p>
<%- else -%>
<br />
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody id="catlist">
<%= render :partial => 'categories_tree' -%>
</tbody>
</table>
<%- end -%>
<br />
<%= link_to 'New Category', new_category_path -%>
Note that we're rendering a partial for the category tree itself. This will allow us to update the view using AJAX when the categories are restructured. So _categories_tree.html.erb looks like this:
<%- @categories.each do |category| -%>
<%= render :partial => 'category_row', :locals => {:category => category} -%>
<%- end -%>
One final partial to enable recursive display magick: _category_row.html.erb>
<tr class="<%= cycle('odd', 'even') -%>" style="cursor: move;" id="category_<%= category.id -%>">
<td style="padding-left: <%= (1 * category.level) + 1 -%>em;">
<%=h category.name -%>
</td>
<td>
<%= link_to 'Show', category -%> |
<%= link_to 'Edit', edit_category_path(category) -%>
</td>
</tr>
<%= draggable_element(
"category_#{category.id}",
:ghosting => true,
:revert => true
)
-%>
<%= drop_receiving_element(
"category_#{category.id}",
:update => 'catlist',
:with => "'subcategory_id=' + element.id",
:url => { :action => 'restructure', :category_id => category.id },
:hoverclass => "row_highlight"
)
-%>
<%- category.children.each do |subcat| -%>
<%= render :partial => 'category_row', :locals => {:category => subcat} -%>
<%- end -%>
Notice the two special blocks here? We're using the draggable_element helper method to define the DOM element tagged with category_# (in our case, the <tr>)as draggable, and the drop_receiving_element helper method to set the row as a receiver for other draggable elements. Since this partial is used to render each of our categories in rows, these definitions will apply to all of the categories in the table.
Each of these helper methods take several parameters, including snap-back behaviour (revert) and a CSS class to apply to an element when a dragged row hovers over it (hoverclass). I encourage you to explore these options to see the sort of nice effects that can be achieved.
When a row is dragged, it will 'ghost' and any receiving row will be highlighted. The result of the drop will be an AJAX call to the restructure method in our controller. Let's add that now.
We need to add a method for handling the restructuring of nested categories, and make a small change to the scoping of the index method:
class CategoriesController < ApplicationController
def restructure
_parent = Category.find(params[:category_id].gsub('category_',''))
_child = Category.find(params[:subcategory_id].gsub('category_',''))
_child.move_to_child_of(_parent)
_child.update_depth
@categories = Category.roots
render :partial => 'categories_tree'
end
This method is simply parsing the category_id and subcategory_id parameters, finding the appropriate category objects, and making the dragged category a child of the drop destination category.
def index
@categories = Category.roots
end
..
end
Rather than populating the @categories instance variable with all categories, the index method is only pulling root nodes (e.g. categories without parents) from the database. Because of our recursive view code, child nodes will be displayed under their parents, indented appropriately.
Here's a quick demo of what the drag and drop looks like. There are a number of improvements that could be made (indent/outdent? multiselect?) but I'm leaving those up to you.
![]()
erwin
June 16, 2009 at 10:37 AMvery interesting... thanks a lot I could make it work easily I have some pending issue on it: how do we tak in account the 'root' items , once they have been moved into the tree and we want to move them at the root level .. ex: I want to move Clara, or Boris or Edward.... at the root level.... how to avoid moving a 'root' item inside one of its leaves... (it raises an error..) ex: moving Undecided into Boris.... do you have some source code I could test to help ? yves
![]()
David
June 25, 2009 at 12:59 AMThanks very much for this example. There's not a whole lot out there demo-wise for awesome-nested-set. Your work here is extremely helpful!
![]()
John
July 12, 2009 at 9:02 AMI don't see where in this code the example supports the structure of the subcategories within the category? If you drag and drop a subcategory to a specific position within a category, how can you pass this to the restructure and then process the order in restructure, at the same time you are processing the category it was added to? I assume you need to do some sort of move_to_right_of, but not sure how to get this info to the action from the interface...