# Turbo streams can be broadcast directly from models that include this module (this is automatically done for Active Records). # This makes it convenient to execute both synchronous and asynchronous updates, and render directly from callbacks in models # or from controllers or jobs that act on those models. Here's an example: # # class Clearance < ApplicationRecord # belongs_to :petitioner, class_name: "Contact" # belongs_to :examiner, class_name: "User" # # after_create_commit :broadcast_later # # private # def broadcast_later # broadcast_prepend_later_to examiner.identity, :clearances # end # end # # This is an example from [HEY](https://hey.com), and the clearance is the model that drives # [the screener](https://hey.com/features/the-screener/), which gives users the power to deny first-time senders (petitioners) # access to their attention (as the examiner). When a new clearance is created upon receipt of an email from a first-time # sender, that'll trigger the call to broadcast_later, which in turn invokes broadcast_prepend_later_to. # # That method enqueues a Turbo::Streams::ActionBroadcastJob for the prepend, which will render the partial for clearance # (it knows which by calling Clearance#to_partial_path, which in this case returns clearances/_clearance.html.erb), # send that to all users that have subscribed to updates (using turbo_stream_from(examiner.identity, :clearances) in a view) # using the Turbo::StreamsChannel under the stream name derived from [ examiner.identity, :clearances ], # and finally prepend the result of that partial rendering to the target identified with the dom id "clearances" # (which is derived by default from the plural model name of the model, but can be overwritten). # # There are four basic actions you can broadcast: remove, replace, append, and # prepend. As a rule, you should use the _later versions of everything except for remove when broadcasting # within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down # execution. You don't need to do this for remove, since only the dom id for the model is used. # # 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. module Turbo::Broadcastable extend ActiveSupport::Concern module ClassMethods # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the # stream symbol invocation. By default, the creates are appended to a dom id target name derived from # the model's plural name. The insertion can also be made to be a prepend by overwriting insertion and # the target dom id overwritten by passing target. Examples: # # class Message < ApplicationRecord # belongs_to :board # broadcasts_to :board # end # # class Message < ApplicationRecord # belongs_to :board # broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages" # end def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default) after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target } after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream) } after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) } end # Same as #broadcasts_to, but the designated stream is automatically set to the current model. def broadcasts(inserts_by: :append, target: broadcast_target_default) after_create_commit -> { broadcast_action_later action: inserts_by, target: target } after_update_commit -> { broadcast_replace_later } after_destroy_commit -> { broadcast_remove } end # All default targets will use the return of this method. Overwrite if you want something else than model_name.plural. def broadcast_target_default model_name.plural end end # Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables. # Example: # # # Sends to the stream named "identity:2:clearances" # clearance.broadcast_remove_to examiner.identity, :clearances def broadcast_remove_to(*streamables) Turbo::StreamsChannel.broadcast_remove_to *streamables, target: self end # Same as #broadcast_remove_to, but the designated stream is automatically set to the current model. def broadcast_remove broadcast_remove_to self end # Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed # streamables. The rendering parameters can be set by appending named arguments to the call. Examples: # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_replace_to examiner.identity, :clearances # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 } def broadcast_replace_to(*streamables, **rendering) Turbo::StreamsChannel.broadcast_replace_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering) end # Same as #broadcast_replace_to, but the designated stream is automatically set to the current model. def broadcast_replace(**rendering) broadcast_replace_to self, **rendering end # Append a rendering of this broadcastable model to the target identified by it's dom id passed as target # for subscribers of the stream name identified by the passed streamables. The rendering parameters can be set by # appending named arguments to the call. Examples: # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances" # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances", # partial: "clearances/other_partial", locals: { a: 1 } def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering) Turbo::StreamsChannel.broadcast_append_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering) end # Same as #broadcast_append_to, but the designated stream is automatically set to the current model. def broadcast_append(target: broadcast_target_default, **rendering) broadcast_append_to self, target: target, **rendering end # Prepend a rendering of this broadcastable model to the target identified by it's dom id passed as target # for subscribers of the stream name identified by the passed streamables. The rendering parameters can be set by # appending named arguments to the call. Examples: # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances" # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances", # partial: "clearances/other_partial", locals: { a: 1 } def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering) Turbo::StreamsChannel.broadcast_prepend_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering) end # Same as #broadcast_append_to, but the designated stream is automatically set to the current model. def broadcast_prepend(target: broadcast_target_default, **rendering) broadcast_prepend_to self, target: target, **rendering end # Broadcast a named action, allowing for dynamic dispatch, instead of using the concrete action methods. Examples: # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances" def broadcast_action_to(*streamables, action:, target: broadcast_target_default, **rendering) Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering)) end # Same as #broadcast_action_to, but the designated stream is automatically set to the current model. def broadcast_action(action, target: broadcast_target_default, **rendering) broadcast_action_to self, action: action, target: target, **rendering end # Same as broadcast_replace_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_replace_later_to(*streamables, **rendering) Turbo::StreamsChannel.broadcast_replace_later_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering) end # Same as #broadcast_replace_later_to, but the designated stream is automatically set to the current model. def broadcast_replace_later(**rendering) broadcast_replace_later_to self, **rendering end # Same as broadcast_append_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering) Turbo::StreamsChannel.broadcast_append_later_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering) end # Same as #broadcast_append_later_to, but the designated stream is automatically set to the current model. def broadcast_append_later(target: broadcast_target_default, **rendering) broadcast_append_later_to self, target: target, **rendering end # Same as broadcast_prepend_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering) Turbo::StreamsChannel.broadcast_prepend_later_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering) end # Same as #broadcast_prepend_later_to, but the designated stream is automatically set to the current model. def broadcast_prepend_later(target: broadcast_target_default, **rendering) broadcast_prepend_later_to self, target: target, **rendering end # Same as broadcast_action_later_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, **rendering) Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering)) end # Same as #broadcast_action_later_to, but the designated stream is automatically set to the current model. def broadcast_action_later(action:, target: broadcast_target_default, **rendering) broadcast_action_later_to self, action: action, target: target, **rendering end # Render a turbo stream template asynchronously with this broadcastable model passed as the local variable using a # Turbo::Streams::BroadcastJob. Example: # # # Template: entries/_entry.turbo_stream.erb # <%= turbo_stream.remove entry %> # # <%= turbo_stream.append "entries", entry if entry.active? %> # # Sends: # # # # # ...to the stream named "entry:5" def broadcast_render_later(**rendering) broadcast_render_later_to self, **rendering end # Same as broadcast_prepend_to but run with the added option of naming the stream using the passed # streamables. def broadcast_render_later_to(*streamables, **rendering) Turbo::StreamsChannel.broadcast_render_later_to *streamables, **broadcast_rendering_with_defaults(rendering) end private def broadcast_target_default self.class.broadcast_target_default end def broadcast_rendering_with_defaults(options) options.tap do |o| o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.singular.to_sym => self) o[:partial] ||= to_partial_path end end end