Hotwire Discussion

Broadcasting to nested turbo_frame_tag

Hi everyone,
I hope 2022 is off to a good start for you.

I have an issue similar to this one but for some reason, the solution does not seem to work in my case.

Basically, I am trying to get familiar with Hotwire and I want to create a blog that works like a single-page app (so turbo stream the crud to change the content of the turbo_frame_tag without reloading the page) but I also want to try out broadcasting both the posts and the comments in real-time.

The only thing not working is the broadcasting of the comments, broadcasting the posts work. I think it has to do with how I use the turbo_stream_from, turbo_frame_tag and/or the way I broadcast but I can’t seem to get it right.

My current posts/index

<%= turbo_stream_from "posts" %>

<div class="w-full">
  [...]
  <div class="min-w-full">
    <%= turbo_frame_tag :posts do %>
      <%= render @posts %>
    <% end %>
  </div>
</div>

he partial for each post is as follows:

<%= turbo_frame_tag post do %>
  <div class="bg-white w-full p-4 rounded-md mt-2">
    <h1 class="my-5 text-lg font-bold text-gray-700 text-4xl hover:text-blue-400">
      <%= link_to post.title, post_path(post) %>
    </h1>
   [...]
  </div>
<% end %>

the posts/show replaces the current partial of each post with a more detailed version:

<%= turbo_stream_from @post, :comments %>

<%= turbo_frame_tag dom_id(@post) do %>
  [...]
    <div class="w-full bg-white rounded-md p-4 justify-start mt-1">
      <div class="w-full pt-2">
        <turbo-frame id="total_comments" class="mt-2 text-lg text-2xl mt-2 tex-gray-50">
          <%= @post.comments.size %> comments
        </turbo-frame>
        <%= turbo_frame_tag "#{dom_id(@post)}_comments" do %>
          <%= render @post.comments %>
        <% end %>
        [...]
      </div>
    </div>
  </div>
<% end %>

with the respective models being:

class Post < ApplicationRecord
  validates_presence_of :title
  has_rich_text :content
  has_many :comments, dependent: :destroy
  after_create_commit { broadcast_prepend_to "posts" }
  after_update_commit { broadcast_replace_to "posts" }
  after_destroy_commit { broadcast_remove_to "posts" }
end

and

class Comment < ApplicationRecord
  include ActionView::RecordIdentifier

  belongs_to :post
  has_rich_text :content

  after_create_commit do
    broadcast_append_to [post, :comments], target: "#{dom_id(post)}_comments", partial: 'comments/comment'
  end

  after_destroy_commit do
    broadcast_remove_to self
  end
end

The _comment partial is

<%= turbo_frame_tag dom_id comment do %>
  <div class="container w-full mx-auto bg-white p-4 rounded-md mt-2 border-b shadow">
    <%= comment.content %> - <%= time_tag comment.created_at, "data-local": "time-ago" %>
    [...]
  </div>
<% end %>

Any idea what I am doing wrong?
Thanks a lot in advance and have a great day.

Hey. Hoping 2022 is off to a good start for you too.

Have you tried

  after_create_commit do
    broadcast_append_to post, :comments, target: "#{dom_id(post)}_comments", partial: 'comments/comment'
  end

Hi!
Thanks a lot for your message :slight_smile:

I just tried the snippet (the one without the brackets around post and :comments) but it still did not broadcast the new comment.

In the console, It does say something like:

  Rendered comments/_comment.html.erb (Duration: 1.8ms | Allocations: 877)
[ActionCable] Broadcasting to [...some string like Z2lk...]:comments: "<turbo-stream action=\"append\" target=\"post_1_comments\"><template><turbo-frame id=\"comment_39\">\n  <div class=\"container w-full mx-auto bg-white p-4 rounded-md mt-2 border-b shadow\">\n    <div class=\"trix-content\">\n  <div>qwe</div>\n</div>\n - <time datetime=\"2022-01-03T20:26:29Z\" da...
  Rendering comments/create.turbo_stream.erb

Are you returning a turbo_stream format?. Like in the controller do you have something like

class CommentsController
   def create
    # create a comment
    respond_to do |format|
       format.turbo_stream { render :create }
   
    end
   end
end

if so, try removing the file comments/create.turbo_stream.erb or just rename it and see what it does. Like, don’t return a stream response when creating the comment. From my experince with streaming with Turbo. If you had a somemodel/{action}.turbo_stream.erb it will take presedence over the after_create_commit defined inside the model.

Hello!

I am indeed returning a stream format.

My comments#create is

 def create
    @comment = @post.comments.build(comments_params)

    respond_to do |format|
      if @comment.save
        format.turbo_stream
        format.html { redirect_to @post, notice: 'Comment was successfully created.' }
        format.json { render :show, status: :created, location: @comment }
      else
      [...error handling]
      end
    end
  end

with the partial create.turbo_stream itself being

<% if @comment.errors.present? %>

[...some error handling]

<% else %>
  <%= comment_notice_stream(message: :create, status: 'green') %> # a helper to display a notice

  <%= turbo_stream.replace 'new_comment' do %>
    <%= turbo_frame_tag :new_comment %>
  <% end %>

  <%= turbo_stream.append "#{dom_id(@post)}_comments", partial: 'comment', locals: { comment: @comment } %>

  <%= turbo_stream.replace 'total_comments' do %>
    <%= turbo_frame_tag :total_comments, class: 'mt-2 text-lg text-2xl mt-2 tex-gray-50' do %>
      <%= @post.comments.size %> comments
    <% end %>
  <% end %>
<% end %>

When I rename the create.turbo_stream file and/or comment out the format.turbo_stream, and then try creating a comment from a different browser window, the comment gets created but does not show on the page without a reload (which is kind of expected since I removed the turbo_stream) but does not appear on the other browser window either, where I would expect it to be broadcasted. This means that I am probably not broadcasting as I should.

I still get in the console [ActionCable] Broadcasting to [...some string]:comments:.....

Basically what I did is follow this article to get me started with a “spa-like” version of the blog and then tried to tweak it to add some actioncable/broadcasting for the posts/comments.

Thanks again for your help!

Hey!
So I found a way to make it work.

In posts/show, it was a matter of putting the stream inside the turbo_frame:

<%= turbo_frame_tag dom_id(@post) do %>
[...]
    <%= turbo_stream_from @post, :comments %>
    <div class="w-full bg-white rounded-md p-4 justify-start mt-1">
      <div class="w-full pt-2">
        <turbo-frame id="total_comments" class="mt-2 text-lg text-2xl mt-2 tex-gray-50">
          <%= @post.comments.size %> comments
        </turbo-frame>
        <%= turbo_frame_tag "#{dom_id(@post)}_comments" do %>
          <%= render @post.comments %>
        <% end %>

        <div class="w-full mb-5 mt-5">
        <%= turbo_frame_tag :new_comment %>
      </div>
      <%= link_to 'New Comment', new_post_comment_path(@post), data: { turbo_frame: 'new_comment' }, class: 'bg-blue-500 text-white text-center p-2 rounded-md' %>
      </div>
    </div>
[...]
<% end %>

That makes creating a comment work.

And to delete a comment in both windows I needed to add a stream at the beginning of the partial:

<%= turbo_stream_from comment %>
<%= turbo_frame_tag dom_id comment do %>
 [...content]
<% end %>

the only thing left to solve is how to update in both windows the count when broadcasting a new comment:

in posts/show:

        <turbo-frame id="total_comments" class="mt-2 text-lg text-2xl mt-2 tex-gray-50">
          <%= @post.comments.size %> comments
        </turbo-frame>

If you have any idea, let me know :slight_smile:
Have a good day.

1 Like

Hi. This is wonderful, you can define a partial that contains this

inside posts/_comment_count.html.erb or anywhere u would like to define the partial

        <turbo-frame id="total_comments" class="mt-2 text-lg text-2xl mt-2 tex-gray-50">
          <%= post.comments.size %> comments
        </turbo-frame>

Then, in the after_create_commit callback you can stream that specific partial, turbo will replace the correct one

class Comment < ApplicationRecord

  after_create_commit do
    # appending the comment
    broadcast_replace_to [post, :comments], target: "total_comments", partial: "posts/comment_count", locals: { post: post }
   end
end
1 Like

Awesome!
Thanks a lot :slight_smile: It works with the partial you mentioned.
Have a good day.

1 Like