Let\'s say I have a comment model:
class Comment < ActiveRecord::Base
has_many :replies, class: \"Comment\", foreign_key: \"reply_id\"
end
We've done this:
We used the ancestry gem to create a hierarchy-centric dataset, and then outputted with a partial outputting an ordered list
:
#app/views/categories/index.html.erb
<% # collection = ancestry object %>
<%= render partial: "category", locals: { collection: collection } %>
#app/views/categories/_category.html.erb
<ol class="categories">
<% collection.arrange.each do |category, sub_item| %>
<li>
<!-- Category -->
<div class="category">
<%= link_to category.title, edit_admin_category_path(category) %>
<%= link_to "+", admin_category_new_path(category), title: "New Categorgy", data: {placement: "bottom"} %>
<% if category.prime? %>
<%= link_to "", admin_category_path(category), title: "Delete", data: {placement: "bottom", confirm: "Really?"}, method: :delete, class: "icon ion-ios7-close-outline" %>
<% end %>
<!-- Page -->
<%= link_to "", new_admin_category_page_path(category), title: "New Page", data: {placement: "bottom"}, class: "icon ion-compose" %>
</div>
<!-- Pages -->
<%= render partial: "pages", locals: { id: category.name } %>
<!-- Children -->
<% if category.has_children? %>
<%= render partial: "category", locals: { collection: category.children } %>
<% end %>
</li>
<% end %>
</ol>
We also made a nested dropdown:
#app/helpers/application_helper.rb
def nested_dropdown(items)
result = []
items.map do |item, sub_items|
result << [('- ' * item.depth) + item.name, item.id]
result += nested_dropdown(sub_items) unless sub_items.blank?
end
result
end
It seems like what you have is one short step away from what you want. You just need to use recursion to call the same code for each reply as you're calling for the original comments. E.g.
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: @comments %>
</div>
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: comment.replies %>
</div>
NB: this isn't the most efficient way of doing things. Each time you call comment.replies active record will run another database query. There's definitely room for improvement but that's the basic idea anyway.
My approach is to make this done as efficient as possible. First lets address how to do that:
Thinking about that, I have found that most of the people address the first but not the second.So lets start with the easy one. we have to have partial for the comments so referencing the answer of jeanaux
we can use his approach to display the comments and will update it later in the answer
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: @comments %>
</div>
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: comment.replies %>
</div>
We must now retrieve those comments in one query if possible so we can just do this in the controller. to be able to do this all comments and replies should have a commentable_id (and type if polymorphic) so that when we query we can get all comments then group them the way we want.
So if we have a post for example and we want to get all its comments we will say in the controller. @comments = @post.comments.group_by {|c| c.reply_id}
by this we have comments in one query processed to be displayed directly Now we can do this to display them instead of what we previously did
All the comments that are not replies are now in the @comments[nil] as they had no reply_id (NB: I don like the @comments[nil] if anyone has any other suggestion please comment or edit)
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: @comments[nil] %>
</div>
All the replies for each comment will be in the has under the parent comment id
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: @comments[comment.id] %>
</div>
To wrap up:
It seems like you need a self-referential association. Check out the following railscast: http://railscasts.com/episodes/163-self-referential-association
That can be solved with resursion or with a special data structure. Recursion is simpler to implement, whereas a datastructure like the one used by the nested_set
gem is more performant.
First an example how it works in pure Ruby.
class Comment < Struct.new(:content, :replies);
def print_nested(level = 0)
puts "#{' ' * level}#{content}" # handle current comment
if replies
replies.each do |reply|
# here is the list of all nested replies generated, do not care
# about how deep the subtree is, cause recursion...
reply.print_nested(level + 1)
end
end
end
end
Example
comments = [ Comment.new(:c_1, [ Comment.new(:c_1a) ]),
Comment.new(:c_2, [ Comment.new(:c_2a),
Comment.new(:c_2b, [ Comment.new(:c_2bi),
Comment.new(:c_2bii) ]),
Comment.new(:c_2c) ]),
Comment.new(:c_3),
Comment.new(:c_4) ]
comments.each(&:print_nested)
# Output
#
# c_1
# c_1a
# c_2
# c_2a
# c_2b
# c_2bi
# c_2bii
# c_2c
# c_3
# c_4
And now with recursive calls of Rails view partials:
# in your comment show view
<%= render :partial => 'nested_comment', :collection => @comment.replies %>
# recursion in a comments/_nested_comment.html.erb partial
<%= nested_comment.content %>
<%= render :partial => 'nested_comment', :collection => nested_comment.replies %>
Setup your database structure, see the docs: http://rubydoc.info/gems/nested_set/1.7.1/frames That add the something like following (untested) to your app.
# in model
acts_as_nested_set
# in controller
def index
@comment = Comment.root # `root` is provided by the gem
end
# in helper
module NestedSetHelper
def root_node(node, &block)
content_tag(:li, :id => "node_#{node.id}") do
node_tag(node) +
with_output_buffer(&block)
end
end
def render_tree(hash, options = {}, &block)
if hash.present?
content_tag :ul, options do
hash.each do |node, child|
block.call node, render_tree(child, &block)
end
end
end
end
def node_tag(node)
content_tag(:div, node.content)
end
end
# in index view
<ul>
<%= render 'tree', :root => @comment %>
</ul>
# in _tree view
<%= root_node(root) do %>
<%= render_tree root.descendants.arrange do |node, child| %>
<%= content_tag :li, :id => "node_#{node.id}" do %>
<%= node_tag(node) %>
<%= child %>
<% end %>
<% end %>
<% end %>
This code is from an old Rails 3.0 app, slightly change and untested. Therefore it will probably not work out of the box, but should illustrate the idea.
This will be my approach:
Reply has self referential HABTM
class Reply < ActiveRecord::Base
belongs_to :comment
has_and_belongs_to_many :sub_replies,
class_name: 'Reply',
join_table: :replies_sub_replies,
foreign_key: :reply_id,
association_foreign_key: :sub_reply_id
def all_replies(reply = self,all_replies = [])
sub_replies = reply.sub_replies
all_replies << sub_replies
return if sub_replies.count == 0
sub_replies.each do |sr|
if sr.sub_replies.count > 0
all_replies(sr,all_replies)
end
end
return all_replies
end
end
Now to get a reply from a comment etc: