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.

The Model

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.

The View

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.

The Controller

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.

Drag and Drop in Action

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.

Related Articles


Comments

erwin
June 16, 2009 at 10:37 AM

very 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 AM

Thanks 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 AM

I 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...
Leave a Comment


IdolHands.com Spam-o-MeterTM
Bot
Spammer
Moron
Human






* Required fields.