How to render partials recursively with turbo frames and stream

248 views Asked by At
  • Hello, I'm relatively new to Turbo Streams and Frames, and I'm currently facing an issue with rendering replies to comments using Turbo Frames and Streams.

Let me explain my setup. I have a Comment model, and I've utilized the same table to include both comments and their respective replies. Below is how I've implemented it

  • Below is my comment.rb
    class Comment < ApplicationRecord
      belongs_to :post
      belongs_to :user
      has_many :likes, as: :likeable
      scope :ordered, -> { order(id: :desc) }
      belongs_to  :parent, class_name: 'Comment', optional: true
      has_many    :replies, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy
    
      validates :body, presence: true
    end
  • below is my posts show page where where i want to render comments all comments from

        <%= turbo_frame_tag "comments" do%>
          <%= render @comments, post: @post, comment: @comment %>
        <% end %>
    

    -Below is my comment partial where i want every parent comment to have its replies nested under it recursively

    <%= turbo_frame_tag comment do%>
      <div class="">
          <p class="mt-4 text-gray-500 dark:text-gray-400"><%= comment.body %></p>
        </div>
        <%= turbo_frame_tag dom_id(Comment.new)  do%>
          <%= render "comments/like_button", comment: comment%>
        <% end %>
        <% if comment.replies.any? %>
          <% comment.replies.each do |reply| %>
            <div class="ml-4">
              <%= turbo_frame_tag nested_dom_id(comment, "replies") do%>
                <%= render 'comments/comment', comment: reply %>
              <% end %>
            </div>
          <% end %>
        <% end %>
      </div>
    <% end %>
    

    app/views/comments/create_turbo.stream.erb

    <%= turbo_stream.update Comment.new, "" %>
    <% if @comment.parent_id.nil? %>
      <%= turbo_stream.prepend "comments", @comment%>
    <% else %>
      <% @comment.replies.each do |reply| %>
        <%= turbo_stream.prepend nested_dom_id(@comment, "replies") do%>
          <%= render 'comments/comment', comment: reply %>
        <% end %>
      <% end %>
    <% end %>
    
    • The problem is , the child comments only show up well nested after i refresh the page manually, which is not the case i want. what i want is that when i create a reply , i want it to get rendered under its parent without page refresh.
    • Any help will be appreciated
2

There are 2 answers

1
mutebi godfrey On BEST ANSWER
 - I made few changes to make it work
```
<% if @comment.parent_id.nil? %>
  <%= turbo_stream.prepend "comments" do %>
    <%= render @comment %>
  <% end %>
<% else %>
  <%= turbo_stream.after dom_id(@comment.parent) do %>
    <div class="ml-4">
      <%= render @comment %>
    </div>
  <% end %>
<% end %>
```
 - now i can have comments rendered recursively 
2
Alex On

Like this:

# app/views/comments/index.html.erb

<%= link_to "New comment", new_comment_path, data: {turbo_frame: :new_comment} %>

<%= turbo_frame_tag :new_comment, class: "contents" %>

<%= tag.div id: :comments do %>
  <%= render @comments.where(parent_id: nil) %>
<% end %>
# app/views/comments/_comment.html.erb

<%= tag.div id: dom_id(comment) do %>
  <%= comment.body %>

  <%= link_to "Reply", new_comment_path(parent_id: comment),
    data: {turbo_frame: dom_id(comment, :new_comment)} %>

  <%= tag.div id: dom_id(comment, :comments), class: "pl-4" do %>
    <%= render comment.replies %>
  <% end %>

  <%= turbo_frame_tag comment, :new_comment, class: "contents" %>
<% end %>
# app/views/comments/new.html.erb

# grab parent_id from url params
<% @comment.parent_id ||= params[:parent_id] %>

<%= turbo_frame_tag *[@comment.parent, :new_comment].compact, class: "contents" do %>
  <%= render "form", comment: @comment %>
<% end %>
# app/views/comments/_form.html.erb

<%= form_with model: comment, class: "contents" do |f| %>
  <%= f.hidden_field :parent_id %>
  <%= f.text_area :body, placeholder: :comment %>

  <%= f.submit class: "btn-primary" %>
<% end %>
# app/controllers/comments_controller.rb

def create
  @comment = Comment.new(comment_params)

  respond_to do |format|
    if @comment.save
      format.turbo_stream do
        if @comment.parent
          render turbo_stream: [
            # remove reply form
            turbo_stream.update(helpers.dom_id(@comment.parent, :new_comment)),
            # add new reply under the corresponding parent comment
            turbo_stream.append(helpers.dom_id(@comment.parent, :comments), @comment),
          ]
        else
          render turbo_stream: [
            turbo_stream.update(:new_comment),
            turbo_stream.append(:comments, @comment),
          ]
        end
      end
      format.html { redirect_to comments_url, notice: "Created." }
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

Note, turbo-rails v1.5.0 updated turbo_frame_tag to generate the same id as dom_id:
https://github.com/hotwired/turbo-rails/pull/476

Before v1.5.0 you would do this:

<%= turbo_frame_tag :new_comment, comment, class: "contents" %>

<%= turbo_frame_tag *[:new_comment, @comment.parent].compact, class: "contents" do %>
  #...

Result:

result

<div>
  <a data-turbo-frame="new_comment" href="/comments/new">New comment</a>
  <turbo-frame class="contents" id="new_comment"></turbo-frame>

  <div id="comments">
    <div id="comment_39">
      first comment
      <a data-turbo-frame="new_comment_comment_39" href="/comments/new?parent_id=39">Reply</a>
      <div id="comments_comment_39" class="pl-4">
        <div id="comment_41">
          first reply
          <a data-turbo-frame="new_comment_comment_41" href="/comments/new?parent_id=41">Reply</a>
          <div id="comments_comment_41" class="pl-4"></div>
          <turbo-frame class="contents" id="new_comment_comment_41"></turbo-frame>
        </div>
        <div id="comment_42">
          second reply
          <a data-turbo-frame="new_comment_comment_42" href="/comments/new?parent_id=42">Reply</a>
          <div id="comments_comment_42" class="pl-4">
            <div id="comment_43">
              first reply to second reply
              <a data-turbo-frame="new_comment_comment_43" href="/comments/new?parent_id=43">Reply</a>
              <div id="comments_comment_43" class="pl-4"></div>
              <turbo-frame class="contents" id="new_comment_comment_43"></turbo-frame>
            </div>
          </div>
          <turbo-frame class="contents" id="new_comment_comment_42"></turbo-frame>
        </div>
      </div>
      <turbo-frame class="contents" id="new_comment_comment_39"></turbo-frame>
    </div>
    <div id="comment_40">
      second comment
      <a data-turbo-frame="new_comment_comment_40" href="/comments/new?parent_id=40">Reply</a>
      <div id="comments_comment_40" class="pl-4"></div>
      <turbo-frame class="contents" id="new_comment_comment_40"></turbo-frame>
    </div>
  </div>
</div>