In Rails 7/turbo-rails 2, my Comment model is set up to broadcast creates/updates/deletes like this:
broadcasts_to(->(comment) { [comment.target, :comments] },
inserts_by: :prepend, partial: "comments/comment",
locals: { controls: true }, target: "comments")
This has never worked properly.
For most users, the comment form UI is a modal. But our app currently has backup rendering for no-js browsers: in this case, the comment form renders on a separate page; after commit it redirects back to show
the object that has the comments.
So the comments controller responds to CRUD actions with the following:
def refresh_comments_or_redirect_to_show
# Comment broadcasts are sent from the model
respond_to do |format|
# format.turbo_stream expects templates for create/edit/destroy
format.html do
redirect_with_query(controller: @target.show_controller,
action: @target.show_action, id: @target.id)
end
end
end
My understanding is that broadcasting from the model should mean the comments controller CRUD actions should not render a template, but here the CRUD actions redraw the whole page, because they run the format.html
block. If i add a format.turbo_stream
block, it complains "No template".
I would like to have the default broadcasting work without a page redraw, and keep that no-js backup redirect. Does the respond_to
block cancel the broadcast? Is there a workaround?
If i delete the respond_to
block, nothing happens; the broadcast does not work, and the controller complains it has no template to render.
If i instead
broadcasts_to
block in the modelrespond_to
block in the controller, adding: format.turbo_stream do
if action_name == "destroy"
render(turbo_stream: turbo_stream.remove(@comment))
else
render(turbo_stream: turbo_stream.prepend("comments", @comment))
end
end
turbo_stream_from(object, :comments)
in the parent template of the comments
partial (where object
is the comment.target
, in the context of the object show page)...the comment is created/updated/destroyed in the UI for the user doing the CRUD, but seemingly not broadcast. The other user's browser window shows it is subscribed, but the UI updates do not happen.
Currently, class level broadasts
and broadcasts_to
has a bug when used with a :locals
option. On the first broadcast locals hash is updated with a {model: model}
and retained for future broadcasts, which means you're broadcasting the same instance over and over again. This might look like nothing is happening on the front end, which is why you should check "Network tab" > "cable" > "Messages tab" to see what turbo streams you're receiving in the browser.
This is fixed in https://github.com/hotwired/turbo-rails/pull/710. Until it is released you can do the following:
Omit :locals
option if possible.
Don't use class level broadacsts. You can break it down to individual callbacks:
after_create_commit -> { broadcast_prepend_later_to([comment.target, :comments], target: :comments, partial: "comments/comment", locals: {controls: true}) }
after_update_commit -> { broadcast_replace_later_to([comment.target, :comments], target: self, partial: "comments/comment", locals: {controls: true}) }
after_destroy_commit -> { broadcast_remove_to([comment.target, :comments], target: self) }
Patch it:
# config/initializers/turbo_broadcasts_patch.rb
Turbo::Broadcastable.class_eval do
private
def broadcast_rendering_with_defaults(options)
options.tap do |o|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
#- o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self).compact
o[:locals] = (o[:locals] || {}).reverse_merge(model_name.element.to_sym => self).compact
if o[:html] || o[:partial]
return o
elsif o[:template] || o[:renderable]
o[:layout] = false
elsif o[:render] == false
return o
else
# if none of these options are passed in, it will set a partial from #to_partial_path
o[:partial] ||= to_partial_path
end
end
end
end