Ruby on Rails
CategoryTreeUsingActsAsTree

Here’s an example of how to use ActsAsTree to display a list of categories. I’ve seen other ones done that display only subcategories or display all the categories AND the subcategories so that the subs look like roots of the tree.. very ugly. I’ve made this code so that it will list ONLY roots at the top and then children so it looks just like a tree should look. Your roots will have to have parent_id equal to 0.

First, slap this in your view:

<%= display_categories(@categories) %>

This is the heart of the code. You’ll want this in a helper – most likely application helper because you’ll want to pull this from multiple views:

   def display_categories(categories)
     ret = "<ul>" 
     for category in categories
       if category.parent_id == 0
         ret += "<li>" 
         ret += link_to category.name
         ret += find_all_subcategories(category)
         ret += "</li>" 
       end
     end
     ret += "</ul>" 
   end

   def find_all_subcategories(category)
    if category.children.size > 0
      ret = '<ul>'
      category.children.each { |subcat| 
        if subcat.children.size > 0
          ret += '<li>'
          ret += link_to h(subcat.name), :action => 'edit', :id => subcat
          ret += find_all_subcategories(subcat)
          ret += '</li>'
        else
          ret += '<li>'
          ret += link_to h(subcat.name), :action => 'edit', :id => subcat 
          ret += '</li>'
        end
        }
      ret += '</ul>'
    else
      ret = ''
    end
  end

I’ve managed to break the DRY rule in this. If anyone has any suggestions for a better way around this I’d love to hear it, so please comment.

Comments by Mikael Cluseau

So lets refine this code to get DRY again :-)

Warning this is unchecked code.

07-05-18 bugfixes for the first 2 comments.

Refinement 1: simple changes in find_all_subcategories

The first thing to do seems to avoid the repetition for the children.size > 0 case. And to get a bit more rubyist ;-)

   def find_all_subcategories(category)
    ret = ''
    if category.children.any?
      ret << '<ul>'
      category.children.each do |subcat| 
        ret << '<li>'
        ret << link_to h(subcat.name), :action => 'edit', :id => subcat
        if subcat.children.any?
          ret << find_all_subcategories(subcat)
        end
        ret << '</li>'
      end
      ret << '</ul>'
    end
    ret
  end

Prettier isn’t it ?

Refinement 2: removing the li and link_to duplication

We see that we have the same code for each category line :

   def display_categories(categories)
     ..
            ret += "<li>" 
            ret += link_to category.name
            ret += find_all_subcategories(category)
            ret += "</li>" 
     ..
   end

and

   def find_all_subcategories(category)
     ..
        ret << '<li>'
        ret << link_to h(subcat.name), :action => 'edit', :id => subcat
        if subcat.children.any?
          ret << find_all_subcategories(subcat)
        end
        ret << '</li>'
    ..
  end

Ok, well, not exactly the same code.

So, let’s apply these changes :

   def display_categories(categories)
     ret = "<ul>" 
     for category in categories
       if category.parent_id.nil?
         ret << "<li>" 
         ret << link_to h(category.name), :id => category
         ret << find_all_subcategories(category) if category.children.any?
         ret << "</li>" 
       end
     end
     ret << "</ul>" 
   end

   def find_all_subcategories(category)
    ret = '<ul>'
    category.children.each do |subcat| 
      ret << '<li>'
      ret << link_to h(subcat.name), :id => subcat
      ret << find_all_subcategories(subcat) if subcat.children.any?
      ret << '</li>'
    end
    ret << '</ul>'
  end

Ok, now that the code is exactly the same, we can extract it to a method :

   def display_categories(categories)
     ret = "<ul>" 
     for category in categories
       if category.parent_id.nil?
         ret << display_category(category)
       end
     end
     ret << "</ul>" 
   end

   def find_all_subcategories(category)
    ret = '<ul>'
    category.children.each do |subcat|
      ret << display_category(subcat)
    end
    ret << '</ul>'
  end

  def display_category(category)
    ret = "<li>" 
    ret << link_to h(category.name), :id => category
    ret << find_all_subcategories(category) if category.children.any?
    ret << "</li>" 
  end

Refinement 3 : removing the “ul” duplication

I think the parent.nil? is useless in display_categories (because you want a display_categories(Category.find_by_parent_id(nil))). This allows to merge display_categories and find_all_subcategories :

   def display_categories(categories)
     ret = "<ul>" 
     for category in categories
       ret << display_category(category)
     end
     ret << "</ul>" 
   end

  def display_category(category)
    ret = "<li>" 
    ret << link_to h(category.name), :id => category
    ret << display_categories(category.children) if category.children.any?
    ret << "</li>" 
  end

Much shorter isn’t it? :-)

Refinement 4: proper display of the list

I tried revision 3 but Categories would be displayed in the root even if their parent_id’s weren’t 0. Therefore, I just changed the code a bit. One change was in the application_helper.rb and the other in the view.

Application Helper:

  def display_categories(categories, parent_id)
    ret = "<ul>" 
      for category in categories
        if category.parent_id == parent_id
          ret << display_category(category)
        end
      end
    ret << "</ul>" 
  end

  def display_category(category)
    ret = "<li>" 
    ret << link_to(h(category.title), :action => "show", :id => category)
    ret << display_categories(category.children, category.id) if category.children.any?
    ret << "</li>" 
  end

The View:

<%= display_categories(@categories, 0) %>

Comment by TommyBlue

Your code doesn’t work for me: if the root has parent_id 0, Category.root returns nil. So i left root.parent_id NULL and modified the helper code in this way:

  def display_categories(categories, parent_id)
    ret = "<ul>" 
      for category in categories
        if category.parent_id == nil
            category.parent_id = 0
        elsif category.parent_id == parent_id
          ret << display_category(category)
        end
      end
    ret << "</ul>" 
  end

  def display_category(category)
    ret = "<li>" 
    ret << link_to(h(category.title), :action => "show", :id => category)
    ret << display_categories(category.children, category.id) if category.children.any?
    ret << "</li>" 
  end

A Non-List Alternative


by KatCrichton


Here’s a tiny example that uses a block and style to indent the categories…


View


<% render_tree(Section.find_by_name('All')) do | category, depth | %>
  <div style="margin-left: <%= depth * 20 %>px;"><%= link_to(h(category.name), :action => "show", :id => category)%></div>
<% end %>

Controller/Helper


  def render_tree(roots, depth=0, &block)
    roots.children.sort_by {|f| f.name}.each do | child |
      yield(child, depth)
      render_tree(child, depth+1, &block)
    end
  end