Skip to content

Creating a selectbox for a form using ancestry

richardonrails edited this page Jul 12, 2020 · 14 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:
    @categories = 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, &block)
      return ancestry_options(items){ |i| "#{'-' * i.depth} #{i.name}" } unless block_given?

      result = []
      items.map do |item, sub_items|
        result << [yield(item), item.id]
        #this is a recursive call:
        result += ancestry_options(sub_items, &block)
      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.

Option 4: Using arrange_as_array

You can use arrange_as_array to build the list of options easily.

And then define an extra method for the select display name. You can even enforce that the current page's subtree is not displayed, if you want to present only the valid parents:

  • In the form:
  <%= f.collection_select :parent_id, @categories, :id, :name_for_selects, :include_blank => true %>
  • In the model:
class Category < ActiveRecord::Base
  has_ancestry

  def self.arrange_as_array(options={}, hash=nil)                                                                                                                                                            
    hash ||= arrange(options)

    arr = []
    hash.each do |node, children|
      arr << node
      arr += arrange_as_array(options, children) unless children.nil?
    end
    arr
  end

  def name_for_selects
    "#{'-' * depth} #{name}"
  end

  def possible_parents
    parents = Category.arrange_as_array(:order => 'name')
    return new_record? ? parents : parents - subtree
  end
end
  • In the controller:
@categories = Category.arrange_as_array({:order => 'name'}, @category.possible_parents)

Demo with option 4 : https://github.com/doabit/ancestry_selectbox_demo