Skip to content

29 Displaying Threaded Comments

Dave Strus edited this page May 7, 2015 · 1 revision

It's time to display nested comments. First, we'll want to change the default scope in the Comment model.

app/models/comment.rb

  default_scope { order('lft ASC, created_at ASC') }

Next, we need a helper to nest ordered lists (<ol>) in our view based on the way that the awesome_nested_set gem (which acts_as_commentable_with_threading uses under-the-hood) stores nesting data.

Create a new helper module called NestablesHelper. I'm going to punt and give you the entire contents of the helper. Feel free to examine this more on your own.

app/helpers/nestables_helper.rb

module NestablesHelper

  def nested_ol(objects, options = {}, &block)
    objects = objects.order(:lft) if objects.is_a? Class

    return '' if objects.size == 0

    if options[:id].present?
      output = "<ol id=\"#{options.fetch(:id)}\">"
    else
      output = '<ol>'
    end
    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></ol>'
          end
          output << '</li>'
        else
          path << o.parent_id
          output << '<ol>'
        end
      elsif i != 0
        output << '</li>'
      end
      output << "<li data-id=\"#{o.id}\" id=\"#{o.class.to_s.underscore}_li_#{o.id}\" >" + capture(o, path.size - 1, &block)
    end

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

  def sorted_nested_ol(objects, order, &block)
    nested_ol 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)
    return false unless children_of[parent_id]
    children_of[parent_id].each do |o|
      results << o
      recombine_lists(results, children_of, o.id)
    end
  end

end

We'll invoke the nested_ol helper method to display comments in posts/show. The following replaces the entire <ol id="comments"> element.

app/views/posts/show.html.erb

  <%= nested_ol @post.comment_threads, id: 'comments' do |comment| %>
    <div class="tagline" title="<%= comment.created_at %>">
      <%= comment.user.username %>
      <%= time_ago_in_words comment.created_at %> ago
    </div>
    <section class="body">
      <%= comment.body %>
    </section>
    <ul class="actions">
      <li><%= link_to 'reply', new_comment_path(parent_id: comment.id, format: 'js'), remote: true, method: :get %></li>
    </ul>
    <div id="reply-form-<%= comment.id %>" style="display: none">
    </div>
 <% end -%>

How does it look? If everything is working, it's time to commit.

$ git add .
$ git commit -m "Displayed threaded comments."
Clone this wiki locally