Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Nestable, sortable and dragable categories

Nestable, sortable and dragable categories:

In the project I'm working on we wanted to have a Category model which we wanted to be nestable. But we also liked the user to have a draggable interface to manage and rearrange the order of his categories. So we chose awesome_nested_set for the model and jQuery.nestedSortable for the UI.

It took me some time to arrange things to work properly so I wanted to share my work in case it helps anybody.

Before beginning

you might want to take a look at a demo app

  1. go to:
  2. click in show of any group
  3. click in manage categories
  4. reorder as you want and click the save button

You can take a look at the source in:

Initial situation

we're using ruby 1.9.2 & rails 3.1-rc4

class Group < ActiveRecord::Base
  has_many :categories
  # ...

class Category < ActiveRecord::Base
  belongs_to :group
  # ...

The controller

We created an special action in the CategoriesController to manage it:

  # app/controllers/categories_controller.rb
  def reorder
    if request.get?
      @group = Group.find(params[:group_id])
      @categories = @group.categories
      redirect_to categories_path(:group_id =>

(I wanted to use the action for both showing the categories and updating the order, hence the diferentiation between get and post request's)

Don't forget to update your routes:

  # config/routes.rb
  resources :categories do
     collection do
       get 'reorder'
       post 'reorder'



Just a simple view with the categories of the current group and a form that the javascript will use to send the new arrangement of the categories.

%h2= "reordering #{} categories"

    = nested_list @categories do |category|
        %p= "#{}(#{})"

    = form_tag reorder_categories_path(:group_id => do
        = submit_tag 'save'


nested_list is a helper I made to display the categories nested in <ul>'s

It is going to travel across the categories recursively, so it expects to receive the roots categories and not its children

  def nested_list(categories, &block)
    return "" if categories.blank?
    html = "<ul>"
    categories.each do |cat|
      html << "<li id='category_#{}'>"
      html << capture(cat,&block)
      html << nested_list(cat.children, &block)
      html << "</li>"
    html << "</ul>"

Notice that the id attribute in <li id='category_#{}'> it is necessary for jQuery.nestedSortable so it can later serialize the list.

NOTE: this helper suffers from the N+1 queries problem, while I work this out you might want to consider having a look at other solutions such as

The javascript

# app/assets/javascripts/
jQuery ->
    $('#sortable ul:first-child').nestedSortable({
            listType: 'ul',
            items: 'li',
            handle: 'p'

    $("#sortable form").submit ->
        serialization = $("#sortable ul:first-child").nestedSortable('serialize')
        input = $("<input>").attr("type", "hidden").attr("name", "order").val(serialization)
        $(this).append($ input)

We activate the jQuery.nestedSortable plugin and attach a function to trigger when the form is about to be submited.

This function will add an input hidden to the form which will contain the serialization of the categories rearranged. The input has the name 'order' so we receive it in the controller as params[:order] (remember how in the controller we called Category.reorder(params[:controller])?)

Well, we're getting to the tricky part


[example]: Say we had 3 categories ordered like this:

- category_1
- category_2
- category_3
- category_4
- category_5

and the user wants to reorder them like this:

- category_1
-- category_2
--- category_4
--- category_3
- category_5

notice how category_4 goes before category_3

So when the user clicks de 'save' button in the reorder view what we'll find in params[:order] in the controller (product of the call to jQuery.nestedSortable serialization) will be this


A string that represents exactly the order of the categories of the user in a very unpleasant way to work it with. Wouldn't it be much better to have a ruby hash like this one?:

{1=>nil, 2=>1, 4=>2, 3=>2, 5=>nil}

Each key being the id of a category and the value the id of its parent.

Well that's exactly what this method does:

    def convert_url_params_to_hash(params)
      params = Rack::Utils.parse_query(params)
      # in this point _params_ looks like this:
      #     {"category[1]"=>"root", "category[2]"=>"1", "category[4]"=>"2", "category[3]"=>"2", "category[5]"=>"root"} do |k,v|
        cat_id = k.match(/\[(.*?)\]/)[1]
        parent_id = v=='root' ? nil : v.to_i
        {cat_id.to_i => parent_id}

The model category.rb


class Category < ActiveRecord::Base
    def self.reorder(params)
      categories = Category.find(params.keys)
      ActiveRecord::Base.transaction do
        self.restructure_ancestry(categories, params)
        self.sort(categories, params)

So basically what this method will do 2 things:

  1. restructure the ancestry of the categories, ie. making sure that each category has for parent the category it should have (according to the params)
  2. sort the categories. Remember how in the exmple category_4 came before category_3? Well this function will take care of it.

Lets take a look at them.


    def self.restructure_ancestry(categories, params)
      categories.each do |cat|
        cat.update_attributes({:parent_id => params[]})

Pretty straightforward, update the parent_id of each category.


The job of this method is to ask each category to sort itself with respect to its siblings. Lets take a look at the code and then I'll explain it with the help of the example:

    def self.sort(categories, params)
      hierarchy = convert_params_to_hierarchy(params)
      categories.each do |cat|

forget about the convert_params_to_hierarchy method for the moment

In the example category_1 and category_5 are siblings. This sort method will begin asking the category_1 to position itself before category_5. In code it would look like this:


The result of executing the categories.each loop with our example would be like this:


Now at this point you might have figured out what convert_params_to_hierarchy does (as well as a few potetion drawbacks).


This method converts our params hash

{1=>nil, 2=>1, 4=>2, 3=>2, 5=>nil} into another hash that represents the set hierarchy

{nil=>[1, 5], 1=>[2], 2=>[4, 3]} where each key is the id of a category (nil being the root category) and the value a list with the id's of each child

    def convert_params_to_hierarchy(params)

the code of hash_revert I took it from


Again this code I took it from elsewhere, from the wiki of the ancestry gem, and added a few modifications:

    def sort(ordered_siblings_ids)
      return if ordered_siblings_ids.length==1
      if ordered_siblings_ids.first == id
        move_to_left_of siblings.find(ordered_siblings_ids.second)
        move_to_right_of siblings.find(ordered_siblings_ids[ordered_siblings_ids.index(id) - 1])

Moving everything into a module

All this logic can be moved into a separate module to be easily included. You can take a look a the result here: app/models/category_sortable.rb

Now in your category.rb you can include CategorySortable


This comment has been minimized.

Show comment Hide comment

kyriacos Aug 7, 2011

Thanks for posting this. I was looking for something like this some time ago. Now i know where to look next time.

Thanks looks good and has coffeescript too ;)

kyriacos commented Aug 7, 2011

Thanks for posting this. I was looking for something like this some time ago. Now i know where to look next time.

Thanks looks good and has coffeescript too ;)


This comment has been minimized.

Show comment Hide comment

ariera Aug 8, 2011

Glad to hear it : )


ariera commented Aug 8, 2011

Glad to hear it : )


This comment has been minimized.

Show comment Hide comment

kyriacos Aug 8, 2011

Forgot to add.
Really nice write up and explanation.

When i found the link i thought it would just be a bunch of code with some comments. so yeah....:) hehe

kyriacos commented Aug 8, 2011

Forgot to add.
Really nice write up and explanation.

When i found the link i thought it would just be a bunch of code with some comments. so yeah....:) hehe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment