# 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).
#
# You can also choose to render html instead of a partial inside of a broadcast
# you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
#
# class Message < ApplicationRecord
# belongs_to :user
#
# after_create_commit :update_message_count
#
# private
# def update_message_count
# broadcast_update_to(user, :messages, target: "message-count", html: "
#{user.messages.count}
")
# end
# end
#
# If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the `template:` option.
# Again, only to any broadcast method that accepts the `**rendering` argument. Example:
#
# class Message < ApplicationRecord
# belongs_to :user
#
# after_create_commit :update_message
#
# private
# def update_message
# broadcast_replace_to(user, :message, target: "message", template: "messages/show", locals: { message: self })
# end
# end
#
# If you want to render a renderable object you can use the `renderable:` option.
#
# class Message < ApplicationRecord
# belongs_to :user
#
# after_create_commit :update_message
#
# private
# def update_message
# broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
# end
# end
#
# 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,
# broadcast_render_to broadcast_render_later, and 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 inserts_by 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
#
# class Message < ApplicationRecord
# belongs_to :board
# broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
# end
def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default, **rendering)
after_create_commit -> { broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering) }
after_update_commit -> { broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
after_destroy_commit -> { broadcast_remove_to(stream.try(:call, self) || send(stream)) }
end
# Same as #broadcasts_to, but the designated stream for updates and destroys is automatically set to
# the current model, for creates - to the model plural name, which can be overriden by passing stream.
def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcast_target_default, **rendering)
after_create_commit -> { broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering) }
after_update_commit -> { broadcast_replace_later(**rendering) }
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, target: self)
Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target)
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 My Clearance
# # to the stream named "identity:2:clearances"
# clearance.broadcast_replace_to examiner.identity, :clearances
#
# # Sends Other partial
# # 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
# Update 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 My Clearance
# # to the stream named "identity:2:clearances"
# clearance.broadcast_update_to examiner.identity, :clearances
#
# # Sends Other partial
# # to the stream named "identity:2:clearances"
# clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_update_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
end
# Same as #broadcast_update_to, but the designated stream is automatically set to the current model.
def broadcast_update(**rendering)
broadcast_update_to self, **rendering
end
# Insert a rendering of this broadcastable model before 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 My Clearance
# # to the stream named "identity:2:clearances"
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5"
#
# # Sends Other partial
# # to the stream named "identity:2:clearances"
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
# partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_before_to(*streamables, target:, **rendering)
Turbo::StreamsChannel.broadcast_before_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
end
# Insert a rendering of this broadcastable model after 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 My Clearance
# # to the stream named "identity:2:clearances"
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5"
#
# # Sends Other partial
# # to the stream named "identity:2:clearances"
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
# partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_after_to(*streamables, target:, **rendering)
Turbo::StreamsChannel.broadcast_after_to(*streamables, target: target, **broadcast_rendering_with_defaults(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 My Clearance
# # to the stream named "identity:2:clearances"
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances"
#
# # Sends Other partial
# # 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 My Clearance
# # to the stream named "identity:2:clearances"
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances"
#
# # Sends Other partial
# # 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_prepend_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 My Clearance
# # 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_update_to but run asynchronously via a Turbo::Streams::BroadcastJob.
def broadcast_update_later_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
end
# Same as #broadcast_update_later_to, but the designated stream is automatically set to the current model.
def broadcast_update_later(**rendering)
broadcast_update_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_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 with this broadcastable model passed as the local variable. Example:
#
# # Template: entries/_entry.turbo_stream.erb
# <%= turbo_stream.remove entry %>
#
# <%= turbo_stream.append "entries", entry if entry.active? %>
#
# Sends:
#
#
# My Entry
#
# ...to the stream named "entry:5".
#
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
# desireable 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.
def broadcast_render(**rendering)
broadcast_render_to self, **rendering
end
# Same as broadcast_render but run with the added option of naming the stream using the passed
# streamables.
#
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
# be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
def broadcast_render_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
end
# Same as broadcast_action_to but run asynchronously via a Turbo::Streams::BroadcastJob.
def broadcast_render_later(**rendering)
broadcast_render_later_to self, **rendering
end
# Same as broadcast_render_later 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|
# 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 o[:html] || o[:partial]
return o
elsif o[:template] || o[:renderable]
o[:layout] = false
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