Mr eel

Rails - acts_as_tree Select Helper

Do you have a model with acts_as_tree? At some point you probably might want to generate a select with a representation of your tree — say categories and sub-categories for blog posts or something. I’ve seen some slightly nasty ways of doing this, so I thought I might try my hand at something cleaner.

Firstly, the most efficient way of building the options for the select is via recursion. Secondly, you shouldn’t try to do it with one method, otherwise you’ll end up checking conditions repeatedly — Is this the top level? Do I have to write out the select open or select close? — when you really don’t need to.

So I’ve split my helper into two methods. One to generate the select tag and another for the options. The method for the options recurses, going through each level in the hierarchy.


def select_for_tree(model, name, entries, options = {})
  # Grab the current value of the specified attribute
  current = instance_variable_get("@#{model}").send(name)
  # Build the option tags
  option_tags = options_for_tree(entries, current, 0)
  # Check to see if we need to include a blank entry at the top
  option_tags.insert(0, content_tag(:option)) if options[:include_blank]
  # Build the select tag
  select_tag("#{model}[#{name}]", option_tags, :id => "#{model}_#{name}")
end

def options_for_tree(entries, current, level)
  options = ''
  entries.each do |entry|
    attrs = {:value => entry.id}
    attrs[:selected] = 'selected' if entry.id == current
    options < < content_tag(:option, ("- " * level) + entry.name, attrs)
    # This is the bit that does the recursion.
    options << options_for_tree(entry.children, current, level + 1) unless entry.children.empty?
  end
  options
end

Not particularly clever, but it was interesting trying to get something reasonably simple and clean.

Posted on November 27th, 2007 | There are 4 comments

Comments

Max Williams on February 26, 2008

Hi there - thanks for this, it looks very useful, but i’m not sure how to use it (i don’t know what all the arguments, eg entries, should be). Would you mind giving an example?

thanks again
max

Mr eel on February 26, 2008

Hi Max,

You only need to call the first method #select_for_tree. The second method is called by the first, you don’t call it directly.

The first argument is the name of the model and the second is the name of the field. The method uses these to get the current value of that field, so it can mark the corresponding option in the select as being selected.

The third argument is the array of model objects you want to build into a tree inside the select — this method assumes this is an array of models with an acts_as_tree declaration. The last argument is just a hash of options for setting the HTML attributes of the select tag.

< %= select_for_tree('user', 'role_id', @roles) %>

Gives us something like:

All it’s doing there is adding a hypen in front of each entry that belongs to the proceeding one. That way when the user looks at the select, they can see the entries indented in relation to one another.

Max Williams on February 26, 2008

Ah, i see :) Thanks!

Brian Getting on March 27, 2009

Thanks so much for this. I was looking for something exactly like this. In case it helps anyone, I needed a prompt for the drop-downs, so I made the following changes to allow for a prompt and HTML options:

def select_for_tree(model, name, entries, options = {}, prompt = ”)
current = instance_variable_get(”@#{model}”).send(name)
option_tags = options_for_tree(entries, current, 0)
option_tags.insert(0, content_tag(:option, prompt)) unless prompt.blank?
# Build the select tag
options.merge!({:id => “#{model}_#{name}”})
select_tag(”#{model}[#{name}]“, option_tags, options)
end

def options_for_tree(entries, current, level)
options = ”
entries.each do |entry|
attrs = {:value => entry.id}
attrs[:selected] = ’selected’ if entry.id == current
options << content_tag(:option, (”- ” * level) + entry.name, attrs)
# This is the bit that does the recursion.
options << options_for_tree(entry.children, current, level + 1) unless entry.children.empty?
end
options
end

Add a comment

All contents © 2005—2007 Luke Matthew Sutton