From f45749f0fe23f4d93fca2c1ebb31187908889d03 Mon Sep 17 00:00:00 2001 From: David Alejandro <15317732+davidalejandroaguilar@users.noreply.github.com> Date: Fri, 3 Feb 2023 22:14:15 -0800 Subject: [PATCH] Allow broadcasting actions without rendering, and passing stream tag attributes In order to allow custom Turbo actions that don't require a template and work only via attributes passed to the turbo_stream_tag. --- app/channels/turbo/streams/broadcasts.rb | 9 +- .../turbo/streams/action_broadcast_job.rb | 4 +- app/models/concerns/turbo/broadcastable.rb | 18 ++-- test/streams/broadcastable_test.rb | 59 +++++++++++++ test/system/broadcasts_test.rb | 87 ++++++++++++++++++- 5 files changed, 159 insertions(+), 18 deletions(-) diff --git a/app/channels/turbo/streams/broadcasts.rb b/app/channels/turbo/streams/broadcasts.rb index f3aabd39..278e0fbf 100644 --- a/app/channels/turbo/streams/broadcasts.rb +++ b/app/channels/turbo/streams/broadcasts.rb @@ -33,9 +33,10 @@ def broadcast_prepend_to(*streamables, **opts) broadcast_action_to(*streamables, action: :prepend, **opts) end - def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering) + def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering) broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template: - rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil) + rendering.delete(:content) || rendering.delete(:html) || (rendering[:render] != false && rendering.any? ? render_format(:html, **rendering) : nil), + **attributes )) end @@ -63,9 +64,9 @@ def broadcast_prepend_later_to(*streamables, **opts) broadcast_action_later_to(*streamables, action: :prepend, **opts) end - def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering) + def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering) Turbo::Streams::ActionBroadcastJob.perform_later \ - stream_name_from(streamables), action: action, target: target, targets: targets, **rendering + stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering end def broadcast_render_to(*streamables, **rendering) diff --git a/app/jobs/turbo/streams/action_broadcast_job.rb b/app/jobs/turbo/streams/action_broadcast_job.rb index a47d7bd7..ae734b67 100644 --- a/app/jobs/turbo/streams/action_broadcast_job.rb +++ b/app/jobs/turbo/streams/action_broadcast_job.rb @@ -2,7 +2,7 @@ class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base discard_on ActiveJob::DeserializationError - def perform(stream, action:, target:, **rendering) - Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering + def perform(stream, action:, target:, attributes: {}, **rendering) + Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, attributes: attributes, **rendering end end diff --git a/app/models/concerns/turbo/broadcastable.rb b/app/models/concerns/turbo/broadcastable.rb index d6c7fe04..2af2eeea 100644 --- a/app/models/concerns/turbo/broadcastable.rb +++ b/app/models/concerns/turbo/broadcastable.rb @@ -249,13 +249,13 @@ def broadcast_prepend(target: broadcast_target_default, **rendering) # # 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)) + 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)) 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 + def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering) + broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering end @@ -300,13 +300,13 @@ def broadcast_prepend_later(target: broadcast_target_default, **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)) + 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)) 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 + def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering) + broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering end # Render a turbo stream template with this broadcastable model passed as the local variable. Example: @@ -367,6 +367,8 @@ def broadcast_rendering_with_defaults(options) 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 diff --git a/test/streams/broadcastable_test.rb b/test/streams/broadcastable_test.rb index 69478f92..c20f1c7d 100644 --- a/test/streams/broadcastable_test.rb +++ b/test/streams/broadcastable_test.rb @@ -108,6 +108,65 @@ class Turbo::BroadcastableTest < ActionCable::Channel::TestCase end end + test "broadcasting action with attributes" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", target: "messages", template: render(@message), "data-foo" => "bar") do + @message.broadcast_action "prepend", target: "messages", attributes: { "data-foo" => "bar" } + end + end + + test "broadcasting action with no rendering" do + assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", target: "messages", template: nil) do + @message.broadcast_action "prepend", target: "messages", render: false + end + end + + test "broadcasting action to with attributes" do + assert_broadcast_on "stream", turbo_stream_action_tag("prepend", target: "messages", template: render(@message), "data-foo" => "bar") do + @message.broadcast_action_to "stream", action: "prepend", attributes: { "data-foo" => "bar" } + end + end + + test "broadcasting action to with no rendering" do + assert_broadcast_on "stream", turbo_stream_action_tag("prepend", target: "messages", template: nil) do + @message.broadcast_action_to "stream", action: "prepend", render: false + end + end + + # The below commented tests pass if you change `Turbo::Streams::Broadcasts#broadcast_action_later_to`'s + # job call from `perform_later` to `perform_now`, but for some reason `perform_enqueued_jobs` is not running them. + + # test "broadcasting action later to with attributes" do + # assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", target: "messages", template: render(@message), "data-foo" => "bar") do + # perform_enqueued_jobs do + # @message.broadcast_action_later_to @message, action: "prepend", target: "messages", attributes: { "data-foo" => "bar" } + # end + # end + # end + + # test "broadcasting action later to with no rendering" do + # assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", target: "messages", template: nil) do + # perform_enqueued_jobs do + # @message.broadcast_action_later_to @message, action: "prepend", target: "messages", render: false + # end + # end + # end + + # test "broadcasting action later with attributes" do + # assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", target: "messages", template: render(@message), "data-foo" => "bar") do + # perform_enqueued_jobs do + # @message.broadcast_action_later action: "prepend", target: "messages", attributes: { "data-foo" => "bar" } + # end + # end + # end + + # test "broadcasting action later with no rendering" do + # assert_broadcast_on @message.to_gid_param, turbo_stream_action_tag("prepend", target: "messages", template: nil) do + # perform_enqueued_jobs do + # @message.broadcast_action_later action: "prepend", target: "messages", render: false + # end + # end + # end + test "render correct local name in partial for namespaced models" do @profile = Users::Profile.new(id: 1, name: "Ryan") assert_broadcast_on @profile.to_param, turbo_stream_action_tag("replace", target: "users_profile_1", template: "

Ryan

\n") do diff --git a/test/system/broadcasts_test.rb b/test/system/broadcasts_test.rb index 7babcbbc..2f324003 100644 --- a/test/system/broadcasts_test.rb +++ b/test/system/broadcasts_test.rb @@ -1,6 +1,8 @@ require "application_system_test_case" class BroadcastsTest < ApplicationSystemTestCase + include ActiveJob::TestHelper + test "Message broadcasts Turbo Streams" do visit messages_path wait_for_stream_to_be_connected @@ -22,21 +24,82 @@ class BroadcastsTest < ApplicationSystemTestCase test "Message broadcasts with renderable: render option" do visit messages_path wait_for_stream_to_be_connected - + assert_broadcasts_text "Test message", to: :messages do |text, target| Message.create(content: "Ignored").broadcast_append_to(target, renderable: MessageComponent.new(text)) end end - + test "Does not render the layout twice when passed a component" do visit messages_path wait_for_stream_to_be_connected - + Message.create(content: "Ignored").broadcast_append_to(:messages, renderable: MessageComponent.new("test")) - + assert_selector("title", count: 1, visible: false, text: "Dummy") end + test "Message broadcasts with extra attributes to turbo stream tag" do + visit messages_path + wait_for_stream_to_be_connected + + assert_broadcasts_text "Message 1", to: :messages do |text, target| + Message.create(content: text).broadcast_action_to(target, action: :append, attributes: { "data-foo": "bar" }) + end + end + + test "Message broadcasts with correct extra attributes to turbo stream tag" do + visit messages_path + wait_for_stream_to_be_connected + + assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target| + Message.create(content: text).broadcast_action_to(target, action: :test, attributes: { attr_key => attr_value }) + end + end + + test "Message broadcasts with no rendering" do + visit messages_path + wait_for_stream_to_be_connected + + assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target| + Message.create(content: text).broadcast_action_to(target, action: :test, render: false, partial: "non_existant", attributes: { attr_key => attr_value }) + end + end + + test "Message broadcasts later with extra attributes to turbo stream tag" do + visit messages_path + wait_for_stream_to_be_connected + + perform_enqueued_jobs do + assert_broadcasts_text "Message 1", to: :messages do |text, target| + Message.create(content: text).broadcast_action_later_to(target, action: :append, attributes: { "data-foo": "bar" }) + end + end + end + + + test "Message broadcasts later with correct extra attributes to turbo stream tag" do + visit messages_path + wait_for_stream_to_be_connected + + perform_enqueued_jobs do + assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target| + Message.create(content: text).broadcast_action_later_to(target, action: :test, attributes: { attr_key => attr_value }) + end + end + end + + test "Message broadcasts later with no rendering" do + visit messages_path + wait_for_stream_to_be_connected + + perform_enqueued_jobs do + assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target| + Message.create(content: text).broadcast_action_to(target, action: :test, render: false, partial: "non_existant", attributes: { attr_key => attr_value }) + end + end + end + test "Users::Profile broadcasts Turbo Streams" do visit users_profiles_path wait_for_stream_to_be_connected @@ -68,4 +131,20 @@ def assert_broadcasts_text(text, to:, &block) within(:element, id: to) { assert_text text } end + + def assert_forwards_turbo_stream_tag_attribute(attr_key:, attr_value:, to:, &block) + execute_script(<<~SCRIPT) + Turbo.StreamActions.test = function () { + const attribute = this.getAttribute('#{attr_key}') + + document.getElementById('#{to}').innerHTML = attribute + } + SCRIPT + + within(:element, id: to) { assert_no_text attr_value } + + [attr_key, attr_value, to].yield_self(&block) + + within(:element, id: to) { assert_text attr_value } + end end