Ruby on Rails
CategoryTreeUsingActsAsTree (Version #21)

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:

<pre><%= display_categories(@categories) %></pre>

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:

<pre> 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 += "

"
end
end
ret += "

"
end

def find_all_subcategories(category)
if category.children.size > 0
ret = ‘


    category.children.each { |subcat|
    if subcat.children.size > 0
    ret += ‘

  • ret += link_to h(subcat.name), :action => ‘edit’, :id => subcat
    ret += find_all_subcategories(subcat)
    ret += ’


  • else
    ret += ‘

  • ret += link_to h(subcat.name), :action => ‘edit’, :id => subcat
    ret += ’


  • end
    }
    ret += ’


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 ;-)

<pre> 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 << '


end
ret << ’


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 :

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

"
..
end

and

<pre> 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 << '


..
end

Ok, well, not exactly the same code.

  • The first method’s link_to has to h to escape special chars, but this is a bug introduced by the repetition (the though that lead to escaping in the second method hasn’t been applied to the first method).
  • There is a if before the call to find_all_subcategories. In fact, this call could be removed since it is checked by find_all_subcategories’ first couple of lines. But doing this check before entering find_all_subcategories is (1) more optimal and (2) could remove the whole if statement.
  • There is a :action => ‘edit’ that can’t be understood here… and there is not :id in the link generated by display_categories.
  • I also correct something : a category without a parent must have its parent_id to nil, not 0, otherwise you break the foreign key constraint.

So, let’s apply these changes :

<pre> 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 << "

"
end
end
ret << "

"
end

def find_all_subcategories(category)
ret = ‘


    category.children.each do |subcat|
    ret << ‘

  • ret << link_to h(subcat.name), :id => subcat
    ret << find_all_subcategories(subcat) if subcat.children.any?
    ret << ’


  • end
    ret << ’


end

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

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

"
end

def find_all_subcategories(category)
ret = ‘


    category.children.each do |subcat|
    ret << display_category(subcat)
    end
    ret << ’


end

def display_category(category) ret = “
  • ” ret << link_to h(category.name), :id => category ret << find_all_subcategories(category) if category.children.any? ret << “
  • ” 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 :

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

    "
    end

    def display_category(category) ret = “
  • ” ret << link_to h(category.name), :id => category ret << display_categories(category.children) if category.children.any? ret << “
  • ” 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:

    <pre> 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 << "

    "
    end

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

    The View:
    <pre> <%= display_categories(@categories, 0) %> </pre>

    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:

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

    "
    end

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

    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:

    <pre><%= display_categories(@categories) %></pre>

    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:

    <pre> 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 += "

    "
    end
    end
    ret += "

    "
    end

    def find_all_subcategories(category)
    if category.children.size > 0
    ret = ‘


      category.children.each { |subcat|
      if subcat.children.size > 0
      ret += ‘

    • ret += link_to h(subcat.name), :action => ‘edit’, :id => subcat
      ret += find_all_subcategories(subcat)
      ret += ’


    • else
      ret += ‘

    • ret += link_to h(subcat.name), :action => ‘edit’, :id => subcat
      ret += ’


    • end
      }
      ret += ’


    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 ;-)

    <pre> 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 << '


    end
    ret << ’


    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 :

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

    "
    ..
    end

    and

    <pre> 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 << '


    ..
    end

    Ok, well, not exactly the same code.

    • The first method’s link_to has to h to escape special chars, but this is a bug introduced by the repetition (the though that lead to escaping in the second method hasn’t been applied to the first method).
    • There is a if before the call to find_all_subcategories. In fact, this call could be removed since it is checked by find_all_subcategories’ first couple of lines. But doing this check before entering find_all_subcategories is (1) more optimal and (2) could remove the whole if statement.
    • There is a :action => ‘edit’ that can’t be understood here… and there is not :id in the link generated by display_categories.
    • I also correct something : a category without a parent must have its parent_id to nil, not 0, otherwise you break the foreign key constraint.

    So, let’s apply these changes :

    <pre> 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 << "

    "
    end
    end
    ret << "

    "
    end

    def find_all_subcategories(category)
    ret = ‘


      category.children.each do |subcat|
      ret << ‘

    • ret << link_to h(subcat.name), :id => subcat
      ret << find_all_subcategories(subcat) if subcat.children.any?
      ret << ’


    • end
      ret << ’


    end

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

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

    "
    end

    def find_all_subcategories(category)
    ret = ‘


      category.children.each do |subcat|
      ret << display_category(subcat)
      end
      ret << ’


    end

    def display_category(category) ret = “
  • ” ret << link_to h(category.name), :id => category ret << find_all_subcategories(category) if category.children.any? ret << “
  • ” 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 :

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

    "
    end

    def display_category(category) ret = “
  • ” ret << link_to h(category.name), :id => category ret << display_categories(category.children) if category.children.any? ret << “
  • ” 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:

    <pre> 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 << "

    "
    end

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

    The View:
    <pre> <%= display_categories(@categories, 0) %> </pre>

    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:

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

    "
    end

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