Broadcasting turbo_stream templates

Pretty simple question - I find the *.turbo_stream.erb files pretty intuitive for updating the current user’s content.

However, updating everyone in the same way is pretty cumbersome. Is it possible to render a turbo_stream template file, and broadcast it for some identifier?

For example, this template file

<%= turbo_stream.replace dom_id(@package, "change-state"), partial: "packages/change_state_button", locals: { package: @package } %>

This replaces change-state_package_id with the partial. The equivelent broadcast is something like

Turbo::StreamsChannel.broadcast_replace_to model, target: "change-state_package_#{model.id}", partial: "packages/change_state_button", locals: { package: model }

This is, for all intents and purposes, the exact same code, written 2 different ways, in 2 different places. Right now, I can stick the second one in an after_update_commit and everything will work just fine. Can I do the same thing somehow with the template file?

1 Like

Hey there,
same issue :frowning:

were you able to solve it ?

Hey - I finally solved.
The docs:
https://www.rubydoc.info/gems/turbo-rails/0.5.2/Turbo/Broadcastable

In addition to the four basic actions, you can also use broadcast_render_later or broadcast_render_later_to to render a turbo stream template with multiple actions.

Basically - from the model - I’m just rendering template with multple actions to update multiple page elements.

@Audrius, are you able to give a quick example of how to use broadcast_render_later? I’m struggling to use it.

Thanks @tleish, I’m aware of the basics of broadcasting but I’m specifically referring to broadcast_render_later that none of those articles seem to cover. The docs strangely omit an actual example of usage, thus my question to @Audrius.

From the source code:

Note that rendering a turbo-stream inline will cause template rendering to happen synchronously. That is usually not desirable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should be using broadcast_render_later, unless you specifically know why synchronous rendering is needed.

broadcast_render_later and broadcast_render_later_to queue an ActiveJob class using ActiveJob::Enqueuing#perform_later.

def broadcast_render_later_to(*streamables, **rendering)
  Turbo::Streams::BroadcastJob.perform_later stream_name_from(streamables), **rendering
end
# The job that powers the <tt>broadcast_render_later_to</tt> available in <tt>Turbo::Streams::Broadcasts</tt> for rendering
# turbo stream templates.
class Turbo::Streams::BroadcastJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError
  
  def perform(stream, **rendering)
    Turbo::StreamsChannel.broadcast_render_to stream, **rendering
  end
end

You would then need to process the job queue in order to render the template (if not running the job service).

Thanks @tleish, does **rendering take whatever a normal render call takes? I experimented with partial: and template:.

**rendering gets passed to the render method but the partial to render is set to the models partial_path by default so if you want to render something custom it only supports partial: and html: which had to be added in as a special edge case.

// from turbo_rails/models/concerns/turbo/broadcastable.rb
 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)
      # if the html option is passed in it will skip setting a partial from #to_partial_path
      unless o.include?(:html)
        o[:partial] ||= to_partial_path
      end
    end
  end

**rendering gets passed into this method as options

Ah righty, I was hoping it would allow rendering a whole response template like create.turbo_stream.erb etc… I have some quite complex streams for reacting to when things get updated but also reordered etc… I suppose I could just move a lot of that to their own partials that can be referred to from both the template and the model callback. Thanks for your help! :smiley:

1 Like