Recursive Menu

Recursive displaying of a nested set structure can be achieved without recursive code and with a single db access to get data (beware that all_children_count may increase db calls). I use this for a dynamic menu, but of course the same conde can be used with a categories tree too.

Released under MIT license, you can copy and use. Please tell me about any improvement you may discover. (c) Jean-Christophe Michel, Symétrie 2007

Model

class Menu < ActiveRecord::Base
    acts_as_nested_set
end

Database

  create_table "menus" do |t|
    t.column "created_at", :datetime, :null => false
    t.column "updated_at", :datetime, :null => false
    t.column "parent_id", :integer
    t.column "lft", :integer, :null => false
    t.column "rgt", :integer, :null => false
    t.column "lock_version", :integer, :default => 0
  end

Helper

  # meuselect is the id of currently selected menu
  def showmenu(menuselect)
    if menuselect
      @menuselect = Menu.find(menuselect.to_i)
      selectpath = @menuselect.self_and_ancestors
    else
      @menuselect = nil
      selectpath = []
    end
    parents = (selectpath.map{|m| m.parent}+[@menuselect]).uniq-[nil]
    parents_sql_filter = parents.empty? ? '' : " OR parent_id IN (#{parents.map{|p| p.id}.join(',')})"
  
    # retrieve only the menus that need to be open
    allmenus = Menu.find(:all, :conditions => "(parent_id IS NULL #{parents_sql_filter})", :order => 'lft')
    # mark where to open and close html lists
    @menus = []
    allmenus.each_index { |i|
        @menus[i] = {:indent => allmenus[i].level, 
                     :title  => allmenus[i].title,
                     :children_count => allmenus[i].all_children_count,
                     :url  =>  allmenus[i].page ? allmenus[i].page.whole_url : ''}
        @menus[i][:selected] =  allmenus[i] == @menuselect
    }
    allmenus[1..-1].each_index { |i|
      @menus[i][:open] = @menus[i][:indent] > @menus[i-1][:indent]
      @menus[i][:close] = [0, @menus[i][:indent] - @menus[i+1][:indent]].max
    }
    @menus.first[:open] = true
    @menus.last[:open] = false
    @menus.first[:close] = 0
    @menus.last[:close] = 1
    
    render 'menu/showmenu'
  end  

View

menu/showmenu.rhtml

<div id="menu">
<%  @menus.each do |m|
      if m[:open] -%>
  <ul><li>
<%    else -%>
  </li><li>
<%    end -%>
  <%= link_to m[:title], wiki_url( :url => m[:url], :lang => m[:lang] ), \
        { :class => "menu_" + m[:indent].to_s + (m[:selected]?' menu_select':'') + ((m[:children_count] > 0)?' menu_chapter':' menu_item') } -%>
<%    m[:close].times do -%>
  </li></ul>
<%    end
    end -%>
</div>