Nested comments from scratch

后端 未结 9 1121
生来不讨喜
生来不讨喜 2020-12-25 09:03

Let\'s say I have a comment model:

class Comment < ActiveRecord::Base
    has_many :replies, class: \"Comment\", foreign_key: \"reply_id\"
end


        
相关标签:
9条回答
  • 2020-12-25 09:26

    We've done this:

    enter image description here

    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:

    enter image description here

    #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
    
    0 讨论(0)
  • 2020-12-25 09:27

    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.

    0 讨论(0)
  • 2020-12-25 09:28

    My approach is to make this done as efficient as possible. First lets address how to do that:

    1. DRY solution.
    2. Least Number of queries to retrieve the comments.

    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:

    1. We added an object_id in the comment model to be able to retrieve them( if not already there)
    2. We added grouping by reply_id to retrieve the comments with one query and process them for the view.
    3. We added a partial that recursively displays the comments (as proposed by jeanaux).
    0 讨论(0)
  • 2020-12-25 09:30

    It seems like you need a self-referential association. Check out the following railscast: http://railscasts.com/episodes/163-self-referential-association

    0 讨论(0)
  • 2020-12-25 09:36

    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.

    Recursion

    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 %>
    

    Nested Set

    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.

    0 讨论(0)
  • 2020-12-25 09:40

    This will be my approach:

    • I have a Comment Model and a Reply model.
    • Comment has_many association with Reply
    • Reply has belongs_to association with Comment
    • 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:

    • Getting all replies from a comment: @comment.replies
    • Getting the Comment from any reply: @reply.comment
    • Getting the intermediate level of replies from a reply: @reply.sub_replies
    • Getting all levels of replies from a reply: @reply.all_replies
    0 讨论(0)
提交回复
热议问题