How to generate nested unordered list tags with one DB hit

DrewBe121212 edited this page Jan 11, 2014 · 8 revisions

There isn't any helper that generates nested <ul>, <li> tags for you, but you can create your own off this example.

In your controller:

def index
  #one hit to DB, brings back all associated comments
  @sketchbook_comments = @artist.sketchbook_comments.order('lft ASC')
end
In your view, use nested_li helper which generates <ul> and <li> tags for you:
<div class="comments">
  <%= nested_li(@sketchbook_comments) do |comment| %>
    <%= comment.comment %> (what goes in <li> tag)
  <% end %>
</div>

In your helper (based on the each_with_level class method) put:

def nested_li(objects, &block)
  objects = objects.order(:lft) if objects.is_a? Class

  return '' if objects.size == 0

  output = '<ul><li>'
  path = [nil]

  objects.each_with_index do |o, i|
    if o.parent_id != path.last
      # We are on a new level, did we descend or ascend?
      if path.include?(o.parent_id)
        # Remove the wrong trailing path elements
        while path.last != o.parent_id
          path.pop
          output << '</li></ul>'
        end
        output << '</li><li>'
      else
        path << o.parent_id
        output << '<ul><li>'
      end
    elsif i != 0
      output << '</li><li>'
    end
    output << capture(o, path.size - 1, &block)
  end

  output << '</li></ul>' * path.length
  output.html_safe
end

To sort the list first add this helper as well:

def sorted_nested_li(objects, order, &block)
  nested_li sort_list(objects, order), &block
end

private

def sort_list(objects, order)
  objects = objects.order(:lft) if objects.is_a? Class

 # Partition the results
  children_of = {}
  objects.each do |o|
    children_of[ o.parent_id ] ||= []
    children_of[ o.parent_id ] << o
  end

  # Sort each sub-list individually
  children_of.each_value do |children|
    children.sort_by! &order
  end

  # Re-join them into a single list
  results = []
  recombine_lists(results, children_of, nil)

  results
end

def recombine_lists(results, children_of, parent_id)
  if children_of[parent_id]
    children_of[parent_id].each do |o|
      results << o
      recombine_lists(results, children_of, o.id)
    end
  end
end

And call it in the view:

<div class="pages">
  <%= sorted_nested_li(@pages, :title) do |page| %>
    <%= page.title %> (what goes in <li> tag)
  <% end %>
</div>