Ruby on Rails
HowToUseActsAsTree

I couldnt find a tutorial anywhere on ActsAsTree so I decided to write one …I did find a small code snippet on the web – but I thought it would be useful to include something on the Wiki. See also MagicFieldNames.

Suppose you have a tree structure and you want to render a drop down list so the user can choose an option e.g.

<%= render_tree_select(@pages, "page_title") %>

Assuming a database structure of

id (int)
parent_id (int)
page_title (string)

Next, you need to create 2 functions 1 for the helper function and 1 for the recursive function which will fill the tree

def render_tree_select(pages, name)
  ret = ''
  ret += "<select>"
  for page in pages 
    ret += "<option>"
    ret += page[name] if page[name]
    ret += recurse_tree(page, 0, name) if page.children.size>0 
  end 
  ret += "</select>"
end

Next, lets write our recursive function (portions of this were taken from This site

def recurse_tree(page, depth, name)
		depth = depth + 1
		level = "- " * depth
		ret = ''
		if page.children.size > 0
		  page.children.each { |subpage| 
			if subpage.children.size > 0
			  ret += '<option id="'+subpage.id.to_s+'">'
			  ret += h(level + subpage[name])
			  ret += recurse_tree(subpage, depth, name)
			  ret += '</option>'
			else
			  ret += '<option id="'+subpage.id.to_s+'">'
			  ret += h(level + subpage[name])
			  ret += '</option>'
			end
			}
		  ret += ''
		end
end

simply drop in this code into a lib file or your _helper.rb and in the view simply add

<%= render_tree_select(@pages, "page_title") %>

You should get a drop down list with the sub-levels indented – simple


I tried using this and noticed a couple errors with the code. The first would cause it to display subfolders in the main folder column, such as:

first folder
second folder
main folder
- first folder
- second folder

this might be useful to some people, but i corrected it to display each sub folder only once. The second bug would cause it to die if there were more then one level of subfolders.

<pre> def render_tree_select(pages, name) ret = '' ret += "<select>" for page in pages if page.parent_id == nil ret += "<option>" ret += page[name] if page[name] ret += recurse_tree(page, 0, name) if page.children.size>0 end end ret += "

"
end

def recurse_tree(page, depth, name)
depth = depth + 1
level = “- " * depth
ret = ’’
if page.children.size > 0
page.children.each { |subpage|
if subpage.children.size > 0
ret += ’”‘subpage.id.to_s’">’
ret += h(level + subpage[name])
ret += recurse_tree(subpage, depth, name)
ret += ’


else
ret = ‘’subpage.id.to_s+’">’
ret += h(level + subpage[name])
ret += ’


end
}
ret += ’’
end
end

thanks to lodestone for the debugging help



	def getCat(cats, depth)
		retval = []
		for cat in cats
			retval << ["--"*depth + cat.name, cat.id]
			retval += getCat(cat.children, depth+1)
		end
		return retval
	end

	def select_category(parm1, parm2)
		return select(parm1, parm2, <span class="newWikiWord">"--no category--", 0<a href="http://wiki.rubyonrails.org/rails/pages/%22--no+category--%22%2C+0">?</a></span> + getCat(Category.find(:all, :conditions => "parent_id=0"), 0))
	end

Another way to do it
—warren



I have taken the code from above and adapted it to work with Silverstripe’s Tree Control [http://digitaleus.foopad.com/pages/silverstripe-tree-control].

_pages_helper.rb_


module PagesHelper

def render_tree(pages, name)
ret = ‘’
ret += "’tree’>"

for page in pages
if page.parent_id == 0
ret = ‘

  • ’ + page.id.to_s + ‘">’ + page[name] + ‘’
    ret += recurse_tree( page, 1, name )
    end
    end
    ret
    end

    def recurse_tree(page, depth, name)
    ret = ‘’
    if page.children.size < 1
    return ’’
    else
    if depth > 0
    ret += ‘

      end
      depth = depth + 1
      page.children.each { |subpage|
      if subpage.children.size > 0
      ret += ‘
    • ’ h(subpage.id.to_s) + ‘">’ + h(subpage[name]) + ‘’
      ret += recurse_tree(subpage, depth, name)
      ret += ’


    • else
      ret = ‘
    • ’ h(subpage.id.to_s) + ‘">’ + h(subpage[name]) + ’


    • depth = 1
      end
      }
      if(depth > 0)
      ret += ’


    end
    depth = 1
    end
    return ret
    end

    end




  • I’ve modified a little bit the code to build a tree with acts_as_nested_set ActiveRecord. Hope it might help someone!
    Massimo

    
    def render_tree_select(root)
        ret = ''
        ret += "<select name='node'>"
         
        # root node
        ret += '<option value="'+root.id.to_s+'">' 
        ret += root.nome if root.nome   
        ret += '</option>' 
          
        for node in root.direct_children 
            ret += '<option value="'+node.id.to_s+'">' 
            ret += "- " + node.nome if node.nome 
            ret += "</option>"         
            ret += recurse_tree(node, 1, node.nome) if node.direct_children.size>0 
        end 
        ret += "</select>" 
      end
    
      def recurse_tree(node, depth, name)
        depth = depth + 1
        level = "- " * depth
        ret = ''
        children = node.direct_children
        if children.size > 0
          children.each do |subnode| 
            if subnode.direct_children.size > 0
              ret += '<option value="'+subnode.id.to_s+'">'
              ret += level + subnode.nome
              ret += recurse_tree(subnode, depth, name)
              ret += '</option>'
            else
              ret += '<option value="'+subnode.id.to_s+'">'
              ret += level + subnode.nome
              ret += '</option>'
            end
          end
          ret += ''
        end
      end
    

    I ran in the same issue and here is my solution which is flexible. It adds a helper named options_for_tree which acts like options_for_select but it builds an indented collection of select (representing the tree). I added another helper options_from_tree_for_select which acts like options_from_collection_for_select so that you can simply change the call in your views.

    Here is the code that should be put in a helper (may be application_helper.rb):

    
    # override options_for_select to fix any double escaped HTML entities
    def options_for_select(*args)
      fix_double_escape(super(*args))
    end
    
    # _roots_ is a collection of root items that will be traversed
    # other params are the same as options_from_collection_for_select
    # _initial_options_ is a collection of options added in front of the result (for the format see options_for_select)
    # If a block is given, _text_method_ is disabled (you may pass nil) and each item will be passed to the block, the returned value will be stringified and be used as the value. This way you may control the indentation string.
    # example to add the ancestors in the options:
    # options_from_tree_for_select(Category.find_all_by_parent_id(nil), :id, nil) {|item, depth| (item.ancestors.reverse << item).map(&:name).join("->") }
    # simply override the indentation padding:
    # options_from_tree_for_select(Category.find_all_by_parent_id(nil), :id, nil) {|item, depth| "---"*depth + item.name }
    def options_from_tree_for_select(roots, value_method, text_method, selected_value = nil, initial_options = nil)
      options_for_select(options_from_tree(roots, value_method, text_method, initial_options), selected_value)
    end
    	
    def options_from_tree(roots, value_method, text_method, initial_options = nil)
      sub_items = lambda {|items, depth| items.inject([]) {|options, item| options << [block_given? ? yield(item, depth).to_s : ("&nbsp;&nbsp;&nbsp;"*depth + item.send(text_method)), item.send(value_method)]; options += sub_items.call(item.children, depth+1) }}
      (initial_options || []) + sub_items.call(roots, 0)
    end
    

    Now in your views simply put this:

    
      <select name="product[category_id]">
        <%= options_from_tree_for_select(Category.find_roots, :id, :name) %>
      </select>
    

    Notice the find_roots method on the Category model, it simply

    find(:all, :conditions => ‘parent_id IS NULL’)
    or any other way to get only the roots.

    Like I put it in the code comments you can pass a block to drive the way the items in the select are output. This gives you flexibility without hacking into the code. Try the example to experiment.

    Note that because you have a options_from_tree method, you can also use it with ActiveScaffold

    Have fun


    Pascal Hurni

    Pascal’s code above is neat, but I couldn’t get it to take a block, the block_given? is lost on the call to options_from_tree.

    I fixed this by passing the block through.

    
     
    def options_from_tree_for_select(roots, value_method, text_method, selected_value = nil, initial_options = nil, &block)
      options_for_select(options_from_tree(roots, value_method, text_method, block,  initial_options), selected_value)
    end
    	
    def options_from_tree(roots, value_method, text_method, b, initial_options = nil)
      sub_items = lambda {|items, depth| items.inject([]) {|options, item| options << [b ? b.call(item, depth).to_s : ("&nbsp;&nbsp;&nbsp;"*depth + item.send(text_method)), item.send(value_method)]; options += sub_items.call(item.children, depth+1) }}
      (initial_options || []) + sub_items.call(roots, 0)
    end
    

    ..

    In actual fact, though, what I wanted was a simple category select that would allow me to navigate up and down the tree, without showing all sub branches. It should show ancestors, and children from the selected branch. To allow the select to access the highest branch, I included a single root node of '---' with parent_id=nil.

    Code is as follows:

    Controller:

    
    def show
      @cat=Category.find_by_parent_id nil
    end

    def pick
    if params[:id]
    @cat=Category.find params[:id]
    else
    @cat=Category.find_by_parent_id(nil)
    end
    render :partial=>’pick’
    end

    View show.rhtml

    
    <div id='pick_category'>
    	<%=render :partial=>'pick' %>

    Partial pick.rhtml

    
    <%= categoryselect(@cat)>	
    <
    = observe_field ‘category_select’,
    :url=>{:action=>’pick’},
    :update=>’pick_category’,
    :with=>’id’
    %>

    and finally the helper to create the select.
    
    def category_select(cat)
      depth,options=build_ancestors(cat)
      cat.children.each {|c|  options<< ["...."*depth+c.name, c.id]}
      select_tag 'category_select', options_for_select(options, cat.id)
    end
    def build_ancestors(cat)
      if cat.parent_id
        depth, options=build_ancestors(cat.parent)
        return depth+1, options << ["...."*depth+cat.name, cat.id]
      else
        return 1,[ [cat.name, cat.id] ]
      end
    end
    

    Hope this may be of help to someone.
    Tonypm

    Reference to the helper function render_tree_select, need to add the HTML attr value into OPTION tag, in order to get the parent_id upon form submit

    Here’s the updated code
    <pre> def render_tree_select(pages, name, attrname) ret = '' ret += "<select id='"+attrname+"_parent_id' name='"+attrname+"[parent_id]'>" for page in pages if page.parent_id == nil ret += "<option>" ret += page[name] if page[name] ret += "

    "
    ret += recurse_tree(page, 0, name) if page.children.size>0
    end
    end
    ret += "

    "
    end

    def recurse_tree(page, depth, name)
    depth = depth + 1
    level = “- " * depth
    ret = ’’
    if page.children.size > 0
    page.children.each { |subpage|
    if subpage.children.size > 0
    ret += ’”‘subpage.id.to_s’" value=“‘subpage.id.to_s’”>’
    ret += h(level + subpage[name])
    ret += ’


    ret = recurse_tree(subpage, depth, name)
    else
    ret += ‘’
    subpage.id.to_s+’" value=“‘subpage.id.to_s’”>’
    ret += h(level + subpage[name])
    ret += ’


    end
    }
    ret += ’’
    end
    end

    Hope it helps someone

    Ulysses

    Updated on May 21, 2008 11:18 by Pietje Prik (81.83.88.156)