Skip to content

Commit

Permalink
Allow broadcasting actions without rendering, and passing stream tag …
Browse files Browse the repository at this point in the history
…attributes (#427)

In order to allow custom Turbo actions that don't require a template and work only via attributes passed to the turbo_stream_tag.
  • Loading branch information
davidalejandroaguilar authored Oct 19, 2023
1 parent 818d919 commit 1684d35
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 18 deletions.
9 changes: 5 additions & 4 deletions app/channels/turbo/streams/broadcasts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions app/jobs/turbo/streams/action_broadcast_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 10 additions & 8 deletions app/models/concerns/turbo/broadcastable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,13 @@ def broadcast_prepend(target: broadcast_target_default, **rendering)
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
# # 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 <tt>#broadcast_action_to</tt>, 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


Expand Down Expand Up @@ -300,13 +300,13 @@ def broadcast_prepend_later(target: broadcast_target_default, **rendering)
end

# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
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 <tt>#broadcast_action_later_to</tt>, 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:
Expand Down Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions test/streams/broadcastable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,70 @@ 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

test "broadcasting action later to with attributes" do
@message.save!

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
@message.save!

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
@message.save!

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
@message.save!

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: "<p>Ryan</p>\n") do
Expand Down
87 changes: 83 additions & 4 deletions test/system/broadcasts_test.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 1684d35

Please sign in to comment.