From 5199904e8dd98604e6be17bbfab960d0598201a5 Mon Sep 17 00:00:00 2001 From: Juan Ferrari Date: Mon, 16 Sep 2024 17:00:55 -0300 Subject: [PATCH] Support `targets:` in `Broadcastable` methods - Update (#511) * @ghiculescu: hotwired#210 added the ability to set `targets` on a stream tag. But that doesn't work nicely with the `Broadcastable` helper methods. Currently you have to do this to target some `targets`: ```ruby after_update_commit -> { broadcast_update_to self, target: nil, targets: ".class_name" } ``` This PR improves things so that you don't need to provide `target: nil` anymore. -- This PR is a continuation of the work made by @ghiculescu at PR turbo-rails#408 * Add specs --- app/models/concerns/turbo/broadcastable.rb | 50 ++++++---- test/streams/broadcastable_test.rb | 102 +++++++++++++++++++++ 2 files changed, 132 insertions(+), 20 deletions(-) diff --git a/app/models/concerns/turbo/broadcastable.rb b/app/models/concerns/turbo/broadcastable.rb index 073e31a3..b39c186e 100644 --- a/app/models/concerns/turbo/broadcastable.rb +++ b/app/models/concerns/turbo/broadcastable.rb @@ -241,13 +241,13 @@ def suppressed_turbo_broadcasts? # # # 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) unless suppressed_turbo_broadcasts? + def broadcast_remove_to(*streamables, target: self, **rendering) + Turbo::StreamsChannel.broadcast_remove_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_remove_to, but the designated stream is automatically set to the current model. - def broadcast_remove - broadcast_remove_to self + def broadcast_remove(**rendering) + broadcast_remove_to self, **rendering end # Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed @@ -265,7 +265,7 @@ def broadcast_remove # # to the stream named "identity:2:clearances" # clearance.broadcast_replace_to examiner.identity, :clearance, attributes: { method: :morph }, 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_replace_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_replace_to, but the designated stream is automatically set to the current model. @@ -288,7 +288,7 @@ def broadcast_replace(**rendering) # # to the stream named "identity:2:clearances" # # clearance.broadcast_update_to examiner.identity, :clearances, attributes: { method: :morph }, 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_update_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_update_to, but the designated stream is automatically set to the current model. @@ -308,8 +308,10 @@ def broadcast_update(**rendering) # # 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)) + def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering) + raise ArgumentError, "at least one of target or targets is required" unless target || targets + + Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets))) end # Insert a rendering of this broadcastable model after the target identified by it's dom id passed as target @@ -324,8 +326,10 @@ def broadcast_before_to(*streamables, target:, **rendering) # # 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)) + def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering) + raise ArgumentError, "at least one of target or targets is required" unless target || targets + + Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets))) end # Append a rendering of this broadcastable model to the target identified by it's dom id passed as target @@ -341,7 +345,7 @@ def broadcast_after_to(*streamables, target:, **rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_append_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_append_to, but the designated stream is automatically set to the current model. @@ -362,7 +366,7 @@ def broadcast_append(target: broadcast_target_default, **rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_prepend_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_prepend_to, but the designated stream is automatically set to the current model. @@ -389,7 +393,7 @@ def broadcast_refresh # # 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, attributes: {}, **rendering) - Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_action_to, but the designated stream is automatically set to the current model. @@ -399,7 +403,7 @@ def broadcast_action(action, target: broadcast_target_default, attributes: {}, * # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_replace_later_to, but the designated stream is automatically set to the current model. @@ -409,7 +413,7 @@ def broadcast_replace_later(**rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_update_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_update_later_to, but the designated stream is automatically set to the current model. @@ -419,7 +423,7 @@ def broadcast_update_later(**rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_append_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_append_later_to, but the designated stream is automatically set to the current model. @@ -429,7 +433,7 @@ def broadcast_append_later(target: broadcast_target_default, **rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_prepend_later_to, but the designated stream is automatically set to the current model. @@ -449,7 +453,7 @@ def broadcast_refresh_later # Same as broadcast_action_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering) - Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_action_later_to, but the designated stream is automatically set to the current model. @@ -485,7 +489,7 @@ def broadcast_render(**rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_render_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts? end # Same as broadcast_render_to but run asynchronously via a Turbo::Streams::BroadcastJob. @@ -496,7 +500,7 @@ def broadcast_render_later(**rendering) # 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)) unless suppressed_turbo_broadcasts? + Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **extract_options_and_add_target(rendering)) unless suppressed_turbo_broadcasts? end private @@ -504,6 +508,12 @@ def broadcast_target_default self.class.broadcast_target_default end + def extract_options_and_add_target(rendering = {}, target: broadcast_target_default) + broadcast_rendering_with_defaults(rendering).tap do |options| + options[:target] = target if !options.key?(:target) && !options.key?(:targets) + end + 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) diff --git a/test/streams/broadcastable_test.rb b/test/streams/broadcastable_test.rb index 84c79fe5..dbbb1d78 100644 --- a/test/streams/broadcastable_test.rb +++ b/test/streams/broadcastable_test.rb @@ -244,6 +244,108 @@ class Turbo::BroadcastableTest < ActionCable::Channel::TestCase end end + test "broadcast_update to target string" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", target: "unique_id", template: render(@message)) do + @message.broadcast_update target: "unique_id" + end + end + + test "broadcast_update to target object" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", target: "message_1", template: render(@message)) do + @message.broadcast_update target: @message + end + end + + test "broadcast_update to targets" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", targets: ".message_1", template: render(@message)) do + @message.broadcast_update targets: ".message_1" + end + end + + test "broadcast_update_to to target string" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", target: "unique_id", template: render(@message)) do + @message.broadcast_update_to @message, target: "unique_id" + end + end + + test "broadcast_update_to to target object" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", target: "message_1", template: render(@message)) do + @message.broadcast_update_to @message, target: @message + end + end + + test "broadcast_update_to to targets" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", targets: ".message_1", template: render(@message)) do + @message.broadcast_update_to @message, targets: ".message_1" + end + end + + test "broadcast_append to targets" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("append", targets: ".message_1", template: render(@message)) do + @message.broadcast_append targets: ".message_1" + end + end + + test "broadcast_remove targets" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("remove", targets: ".message_1", template: render(@message)) do + @message.broadcast_remove targets: ".message_1" + end + end + + test "broadcast_append targets" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("append", targets: ".message_1", template: render(@message)) do + @message.broadcast_append targets: ".message_1" + end + end + + test "broadcast_prepend targets" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", targets: ".message_1", template: render(@message)) do + @message.broadcast_prepend targets: ".message_1" + end + end + + test "broadcast_before_to targets" do + assert_broadcast_on "stream", turbo_stream_action_tag("before", targets: ".message_1", template: render(@message)) do + @message.broadcast_before_to "stream", targets: ".message_1" + end + end + + test "broadcast_after_to targets" do + assert_broadcast_on "stream", turbo_stream_action_tag("after", targets: ".message_1", template: render(@message)) do + @message.broadcast_after_to "stream", targets: ".message_1" + end + end + + test "broadcast_update_later" do + @message.save! # Need to save the record, otherwise Active Job will not be able to retrieve it + + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", target: "unique_id", template: render(@message)) do + perform_enqueued_jobs do + @message.broadcast_update_later target: "unique_id" + end + end + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", targets: ".message_1", template: render(@message)) do + perform_enqueued_jobs do + @message.broadcast_update_later targets: ".message_1" + end + end + end + + test "broadcast_update_later_to" do + @message.save! + + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", target: "unique_id", template: render(@message)) do + perform_enqueued_jobs do + @message.broadcast_update_later_to @message, target: "unique_id" + end + end + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("update", targets: ".message_1", template: render(@message)) do + perform_enqueued_jobs do + @message.broadcast_update_later_to @message, targets: ".message_1" + end + end + end + test "broadcasting replace morph to stream now" do assert_broadcast_on "stream", turbo_stream_action_tag("replace", target: "message_1", method: :morph, template: render(@message)) do @message.broadcast_replace_to "stream", target: "message_1", attributes: { method: :morph }