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
<%= 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
_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
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 : (" "*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 : (" "*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
select(@cat)>
<%= category
<= 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