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 main problem with some of the options above (at least option2) is that it doesn't keep the order of the category. The second problem I saw was multiple of SQL queries. 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.