Skip to content

Creating a selectbox for a form using ancestry

yaniviny edited this page May 29, 2011 · 12 revisions

Option 1: Adding a cache

Assuming we have a model Category

  • Add a new string field: names_depth_cache
  • Add a before_save to your ancestry model:
    before_save :cache_ancestry
    def cache_ancestry
      self.names_depth_cache = path.map(&:name).join('/')
    end
  • In the controller:
    @categories = Category.order(:names_depth_cache).map { |c| ["-" * c.depth + c.name,c.id] }
  • In the form:
    <%= f.select :parent_id, @categories%>

Option 2: Complicated ruby

  • In the form:
    <%= f.select :parent_id, @categories%>
  • In the controller:
    Category.all.each { |c| c.ancestry = c.ancestry.to_s + (c.ancestry != nil ? "/" : '') + c.id.to_s 
      }.sort {|x,y| x.ancestry <=> y.ancestry 
      }.map{ |c| ["-" * (c.depth - 1) + c.name,c.id] 
      }.unshift(["-- none --", nil])

Option 3: Helper method

The problem with some of the options above (at laest option2) is that it doesn't keep the order of the category. You can bypass this by doing the following:

  • In the form:
    <%= f.select :parent_id, @categories%>
  • In the controller:
    @categories = ancestry_options(Category.scoped.arrange(:order => 'name') {|i| "#{'-' * i.depth} #{i.name}" }

    def ancestry_options(items)
      result = []
      items.map do |item, sub_items|
        result << [yield(item), item.id]
        #this is a recursive call:
        result += ancestry_options(sub_items) {|i| "#{'-' * i.depth} #{i.name}" }
      end
      result
    end

The main benefit of the above's implementation is that it's a single SQL call (for efficiency) and the order remains as it should.