turbo-rails 1.5.0 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +126 -16
- data/app/assets/javascripts/turbo.js +2226 -953
- data/app/assets/javascripts/turbo.min.js +9 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +47 -10
- data/app/channels/turbo/streams_channel.rb +15 -15
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/controllers/turbo/frames/frame_request.rb +2 -2
- data/app/controllers/turbo/native/navigation.rb +17 -11
- data/app/helpers/turbo/drive_helper.rb +72 -14
- data/app/helpers/turbo/frames_helper.rb +8 -8
- data/app/helpers/turbo/streams/action_helper.rb +12 -4
- data/app/helpers/turbo/streams_helper.rb +5 -0
- data/app/javascript/turbo/cable_stream_source_element.js +10 -0
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +201 -42
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +50 -12
- data/app/models/turbo/thread_debouncer.rb +28 -0
- data/config/routes.rb +3 -4
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/broadcastable/test_helper.rb +5 -5
- data/lib/turbo/engine.rb +80 -9
- data/lib/turbo/system_test_helper.rb +128 -0
- data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
- data/lib/turbo/test_assertions.rb +2 -2
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +10 -19
- data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,4 +1,4 @@
|
|
1
|
-
# Provides the broadcast actions in synchronous and
|
1
|
+
# Provides the broadcast actions in synchronous and asynchronous form for the <tt>Turbo::StreamsChannel</tt>.
|
2
2
|
# See <tt>Turbo::Broadcastable</tt> for the user-facing API that invokes these methods with most of the paperwork filled out already.
|
3
3
|
#
|
4
4
|
# Can be used directly using something like <tt>Turbo::StreamsChannel.broadcast_remove_to :entries, target: 1</tt>.
|
@@ -6,7 +6,7 @@ module Turbo::Streams::Broadcasts
|
|
6
6
|
include Turbo::Streams::ActionHelper
|
7
7
|
|
8
8
|
def broadcast_remove_to(*streamables, **opts)
|
9
|
-
broadcast_action_to(*streamables, action: :remove, **opts)
|
9
|
+
broadcast_action_to(*streamables, action: :remove, render: false, **opts)
|
10
10
|
end
|
11
11
|
|
12
12
|
def broadcast_replace_to(*streamables, **opts)
|
@@ -33,10 +33,14 @@ module Turbo::Streams::Broadcasts
|
|
33
33
|
broadcast_action_to(*streamables, action: :prepend, **opts)
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
37
|
-
broadcast_stream_to(*streamables, content:
|
38
|
-
|
39
|
-
|
36
|
+
def broadcast_refresh_to(*streamables, **opts)
|
37
|
+
broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag)
|
38
|
+
end
|
39
|
+
|
40
|
+
def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
|
41
|
+
broadcast_stream_to(*streamables, content: turbo_stream_action_tag(
|
42
|
+
action, target: target, targets: targets, template: render_broadcast_action(rendering), **attributes)
|
43
|
+
)
|
40
44
|
end
|
41
45
|
|
42
46
|
def broadcast_replace_later_to(*streamables, **opts)
|
@@ -63,9 +67,22 @@ module Turbo::Streams::Broadcasts
|
|
63
67
|
broadcast_action_later_to(*streamables, action: :prepend, **opts)
|
64
68
|
end
|
65
69
|
|
66
|
-
def
|
67
|
-
|
68
|
-
stream_name_from(streamables),
|
70
|
+
def broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **opts)
|
71
|
+
refresh_debouncer_for(*streamables, request_id: request_id).debounce do
|
72
|
+
Turbo::Streams::BroadcastStreamJob.perform_later stream_name_from(streamables), content: turbo_stream_refresh_tag(request_id: request_id, **opts).to_str # Sidekiq requires job arguments to be valid JSON types, such as String
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
|
77
|
+
streamables.flatten!
|
78
|
+
streamables.compact_blank!
|
79
|
+
|
80
|
+
if streamables.present?
|
81
|
+
target = convert_to_turbo_stream_dom_id(target)
|
82
|
+
targets = convert_to_turbo_stream_dom_id(targets, include_selector: true)
|
83
|
+
Turbo::Streams::ActionBroadcastJob.perform_later \
|
84
|
+
stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
|
85
|
+
end
|
69
86
|
end
|
70
87
|
|
71
88
|
def broadcast_render_to(*streamables, **rendering)
|
@@ -77,12 +94,32 @@ module Turbo::Streams::Broadcasts
|
|
77
94
|
end
|
78
95
|
|
79
96
|
def broadcast_stream_to(*streamables, content:)
|
80
|
-
|
97
|
+
streamables.flatten!
|
98
|
+
streamables.compact_blank!
|
99
|
+
|
100
|
+
if streamables.present?
|
101
|
+
ActionCable.server.broadcast stream_name_from(streamables), content
|
102
|
+
end
|
81
103
|
end
|
82
104
|
|
105
|
+
def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
|
106
|
+
Turbo::ThreadDebouncer.for("turbo-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
|
107
|
+
end
|
83
108
|
|
84
109
|
private
|
85
110
|
def render_format(format, **rendering)
|
86
111
|
ApplicationController.render(formats: [ format ], **rendering)
|
87
112
|
end
|
113
|
+
|
114
|
+
def render_broadcast_action(rendering)
|
115
|
+
content = rendering.delete(:content)
|
116
|
+
html = rendering.delete(:html)
|
117
|
+
render = rendering.delete(:render)
|
118
|
+
|
119
|
+
if render == false
|
120
|
+
nil
|
121
|
+
else
|
122
|
+
content || html || (render_format(:html, **rendering) if rendering.present?)
|
123
|
+
end
|
124
|
+
end
|
88
125
|
end
|
@@ -9,27 +9,27 @@
|
|
9
9
|
# helper modules like <tt>Turbo::Streams::StreamName</tt>:
|
10
10
|
#
|
11
11
|
# class CustomChannel < ActionCable::Channel::Base
|
12
|
-
#
|
13
|
-
#
|
12
|
+
# extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
|
13
|
+
# include Turbo::Streams::StreamName::ClassMethods
|
14
14
|
#
|
15
|
-
#
|
16
|
-
#
|
15
|
+
# def subscribed
|
16
|
+
# if (stream_name = verified_stream_name_from_params).present? &&
|
17
17
|
# subscription_allowed?
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
18
|
+
# stream_from stream_name
|
19
|
+
# else
|
20
|
+
# reject
|
21
|
+
# end
|
22
|
+
# end
|
23
23
|
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
24
|
+
# def subscription_allowed?
|
25
|
+
# # ...
|
26
|
+
# end
|
27
27
|
# end
|
28
28
|
#
|
29
|
-
#
|
30
|
-
#
|
29
|
+
# This channel can be connected to a web page using <tt>:channel</tt> option in
|
30
|
+
# <tt>turbo_stream_from</tt> helper:
|
31
31
|
#
|
32
|
-
#
|
32
|
+
# <%= turbo_stream_from 'room', channel: CustomChannel %>
|
33
33
|
#
|
34
34
|
class Turbo::StreamsChannel < ActionCable::Channel::Base
|
35
35
|
extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Turbo::RequestIdTracking
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
around_action :turbo_tracking_request_id
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def turbo_tracking_request_id(&block)
|
10
|
+
Turbo.with_request_id(request.headers["X-Turbo-Request-Id"], &block)
|
11
|
+
end
|
12
|
+
end
|
@@ -5,7 +5,7 @@
|
|
5
5
|
# When that header is detected by the controller, we substitute our own minimal layout in place of the
|
6
6
|
# application-supplied layout (since we're only working on an in-page frame, thus can skip the weight of the layout). We
|
7
7
|
# use a minimal layout, rather than avoid the layout entirely, so that it's still possible to render content into the
|
8
|
-
# <tt>head
|
8
|
+
# <tt>head</tt>.
|
9
9
|
#
|
10
10
|
# Accordingly, we ensure that the etag for the page is changed, such that a cache for a minimal-layout request isn't
|
11
11
|
# served on a normal request and vice versa.
|
@@ -24,7 +24,7 @@ module Turbo::Frames::FrameRequest
|
|
24
24
|
layout -> { "turbo_rails/frame" if turbo_frame_request? }
|
25
25
|
etag { :frame if turbo_frame_request? }
|
26
26
|
|
27
|
-
helper_method :turbo_frame_request_id
|
27
|
+
helper_method :turbo_frame_request?, :turbo_frame_request_id
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
@@ -1,43 +1,49 @@
|
|
1
1
|
# Turbo is built to work with native navigation principles and present those alongside what's required for the web. When you
|
2
|
-
# have
|
3
|
-
# requests with three dedicated responses: <tt>recede</tt>, <tt>resume</tt>, <tt>refresh</tt>.
|
2
|
+
# have Hotwire Native clients running (see the Hotwire Native iOS and Hotwire Native Android projects for details),
|
3
|
+
# you can respond to native requests with three dedicated responses: <tt>recede</tt>, <tt>resume</tt>, <tt>refresh</tt>.
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# Hotwire Native Android and Hotwire Native iOS handle these actions automatically.
|
6
6
|
module Turbo::Native::Navigation
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
9
|
included do
|
10
|
-
helper_method :turbo_native_app?
|
10
|
+
helper_method :hotwire_native_app?, :turbo_native_app?
|
11
11
|
end
|
12
12
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
13
|
+
# Hotwire Native applications are identified by having the string "Hotwire Native" as part of their user agent.
|
14
|
+
# Legacy Turbo Native applications use the "Turbo Native" string.
|
15
|
+
def hotwire_native_app?
|
16
|
+
request.user_agent.to_s.match?(/(Turbo|Hotwire) Native/)
|
16
17
|
end
|
17
|
-
|
18
|
-
|
18
|
+
|
19
|
+
alias_method :turbo_native_app?, :hotwire_native_app?
|
20
|
+
|
21
|
+
# Tell the Turbo Native app to dismiss a modal (if presented) or pop a screen off of the navigation stack. Otherwise redirect to the given URL if Turbo Native is not present.
|
19
22
|
def recede_or_redirect_to(url, **options)
|
20
23
|
turbo_native_action_or_redirect url, :recede, :to, options
|
21
24
|
end
|
22
25
|
|
23
|
-
# Tell the Turbo Native app to ignore this navigation.
|
26
|
+
# Tell the Turbo Native app to ignore this navigation, otherwise redirect to the given URL if Turbo Native is not present.
|
24
27
|
def resume_or_redirect_to(url, **options)
|
25
28
|
turbo_native_action_or_redirect url, :resume, :to, options
|
26
29
|
end
|
27
30
|
|
28
|
-
# Tell the Turbo Native app to refresh the current screen.
|
31
|
+
# Tell the Turbo Native app to refresh the current screen, otherwise redirect to the given URL if Turbo Native is not present.
|
29
32
|
def refresh_or_redirect_to(url, **options)
|
30
33
|
turbo_native_action_or_redirect url, :refresh, :to, options
|
31
34
|
end
|
32
35
|
|
36
|
+
# Same as <tt>recede_or_redirect_to</tt> but redirects to the previous page or provided fallback location if the Turbo Native app is not present.
|
33
37
|
def recede_or_redirect_back_or_to(url, **options)
|
34
38
|
turbo_native_action_or_redirect url, :recede, :back, options
|
35
39
|
end
|
36
40
|
|
41
|
+
# Same as <tt>resume_or_redirect_to</tt> but redirects to the previous page or provided fallback location if the Turbo Native app is not present.
|
37
42
|
def resume_or_redirect_back_or_to(url, **options)
|
38
43
|
turbo_native_action_or_redirect url, :resume, :back, options
|
39
44
|
end
|
40
45
|
|
46
|
+
# Same as <tt>refresh_or_redirect_to</tt> but redirects to the previous page or provided fallback location if the Turbo Native app is not present.
|
41
47
|
def refresh_or_redirect_back_or_to(url, **options)
|
42
48
|
turbo_native_action_or_redirect url, :refresh, :back, options
|
43
49
|
end
|
@@ -1,29 +1,87 @@
|
|
1
|
+
# Helpers to configure Turbo Drive via meta directives. They come in two
|
2
|
+
# variants:
|
3
|
+
#
|
4
|
+
# The recommended option is to include +yield :head+ in the +<head>+ section
|
5
|
+
# of the layout. Then you can use the helpers in any view.
|
6
|
+
#
|
7
|
+
# ==== Example
|
8
|
+
#
|
9
|
+
# # app/views/application.html.erb
|
10
|
+
# <html><head><%= yield :head %></head><body><%= yield %></html>
|
11
|
+
#
|
12
|
+
# # app/views/trays/index.html.erb
|
13
|
+
# <% turbo_exempts_page_from_cache %>
|
14
|
+
# <p>Page that shouldn't be cached by Turbo</p>
|
15
|
+
#
|
16
|
+
# Alternatively, you can use the +_tag+ variant of the helpers to only get the
|
17
|
+
# HTML for the meta directive.
|
1
18
|
module Turbo::DriveHelper
|
2
|
-
# Note: These helpers require a +yield :head+ provision in the layout.
|
3
|
-
#
|
4
|
-
# ==== Example
|
5
|
-
#
|
6
|
-
# # app/views/application.html.erb
|
7
|
-
# <html><head><%= yield :head %></head><body><%= yield %></html>
|
8
|
-
#
|
9
|
-
# # app/views/trays/index.html.erb
|
10
|
-
# <% turbo_exempts_page_from_cache %>
|
11
|
-
# <p>Page that shouldn't be cached by Turbo</p>
|
12
|
-
|
13
19
|
# Pages that are more likely than not to be a cache miss can skip turbo cache to avoid visual jitter.
|
14
20
|
# Cannot be used along with +turbo_exempts_page_from_preview+.
|
15
21
|
def turbo_exempts_page_from_cache
|
16
|
-
provide :head,
|
22
|
+
provide :head, turbo_exempts_page_from_cache_tag
|
23
|
+
end
|
24
|
+
|
25
|
+
# See +turbo_exempts_page_from_cache+.
|
26
|
+
def turbo_exempts_page_from_cache_tag
|
27
|
+
tag.meta(name: "turbo-cache-control", content: "no-cache")
|
17
28
|
end
|
18
29
|
|
19
30
|
# Specify that a cached version of the page should not be shown as a preview during an application visit.
|
20
31
|
# Cannot be used along with +turbo_exempts_page_from_cache+.
|
21
32
|
def turbo_exempts_page_from_preview
|
22
|
-
provide :head,
|
33
|
+
provide :head, turbo_exempts_page_from_preview_tag
|
34
|
+
end
|
35
|
+
|
36
|
+
# See +turbo_exempts_page_from_preview+.
|
37
|
+
def turbo_exempts_page_from_preview_tag
|
38
|
+
tag.meta(name: "turbo-cache-control", content: "no-preview")
|
23
39
|
end
|
24
40
|
|
25
41
|
# Force the page, when loaded by Turbo, to be cause a full page reload.
|
26
42
|
def turbo_page_requires_reload
|
27
|
-
provide :head,
|
43
|
+
provide :head, turbo_page_requires_reload_tag
|
44
|
+
end
|
45
|
+
|
46
|
+
# See +turbo_page_requires_reload+.
|
47
|
+
def turbo_page_requires_reload_tag
|
48
|
+
tag.meta(name: "turbo-visit-control", content: "reload")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Configure how to handle page refreshes. A page refresh happens when
|
52
|
+
# Turbo loads the current page again with a *replace* visit:
|
53
|
+
#
|
54
|
+
# ==== Parameters:
|
55
|
+
#
|
56
|
+
# * <tt>method</tt> - Method to update the +<body>+ of the page
|
57
|
+
# during a page refresh. It can be one of:
|
58
|
+
# * +replace:+: Replaces the existing +<body>+ with the new one. This is the
|
59
|
+
# default behavior.
|
60
|
+
# * +morph:+: Morphs the existing +<body>+ into the new one.
|
61
|
+
#
|
62
|
+
# * <tt>scroll</tt> - Controls the scroll behavior when a page refresh happens. It
|
63
|
+
# can be one of:
|
64
|
+
# * +reset:+: Resets scroll to the top, left corner. This is the default.
|
65
|
+
# * +preserve:+: Keeps the scroll.
|
66
|
+
#
|
67
|
+
# ==== Example Usage:
|
68
|
+
#
|
69
|
+
# turbo_refreshes_with(method: :morph, scroll: :preserve)
|
70
|
+
def turbo_refreshes_with(method: :replace, scroll: :reset)
|
71
|
+
provide :head, turbo_refresh_method_tag(method)
|
72
|
+
provide :head, turbo_refresh_scroll_tag(scroll)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Configure method to perform page refreshes. See +turbo_refreshes_with+.
|
76
|
+
def turbo_refresh_method_tag(method = :replace)
|
77
|
+
raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ])
|
78
|
+
tag.meta(name: "turbo-refresh-method", content: method)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Configure scroll strategy for page refreshes. See +turbo_refreshes_with+.
|
82
|
+
def turbo_refresh_scroll_tag(scroll = :reset)
|
83
|
+
raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ])
|
84
|
+
tag.meta(name: "turbo-refresh-scroll", content: scroll)
|
28
85
|
end
|
29
86
|
end
|
87
|
+
|
@@ -2,7 +2,7 @@ module Turbo::FramesHelper
|
|
2
2
|
# Returns a frame tag that can either be used simply to encapsulate frame content or as a lazy-loading container that starts empty but
|
3
3
|
# fetches the URL supplied in the +src+ attribute.
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# ==== Examples
|
6
6
|
#
|
7
7
|
# <%= turbo_frame_tag "tray", src: tray_path(tray) %>
|
8
8
|
# # => <turbo-frame id="tray" src="http://example.com/trays/1"></turbo-frame>
|
@@ -24,19 +24,19 @@ module Turbo::FramesHelper
|
|
24
24
|
# <% end %>
|
25
25
|
# # => <turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
|
26
26
|
#
|
27
|
-
#
|
28
|
-
#
|
27
|
+
# <%= turbo_frame_tag [user_id, "tray"], src: tray_path(tray) %>
|
28
|
+
# # => <turbo-frame id="1_tray" src="http://example.com/trays/1"></turbo-frame>
|
29
|
+
#
|
30
|
+
# The +turbo_frame_tag+ helper will convert the arguments it receives to their
|
31
|
+
# +dom_id+ if applicable to easily generate unique ids for Turbo Frames:
|
29
32
|
#
|
30
33
|
# <%= turbo_frame_tag(Article.find(1)) %>
|
31
34
|
# # => <turbo-frame id="article_1"></turbo-frame>
|
32
35
|
#
|
33
36
|
# <%= turbo_frame_tag(Article.find(1), "comments") %>
|
34
|
-
# # => <turbo-frame id="
|
35
|
-
#
|
36
|
-
# <%= turbo_frame_tag(Article.find(1), Comment.new) %>
|
37
|
-
# # => <turbo-frame id="article_1_new_comment"></turbo-frame>
|
37
|
+
# # => <turbo-frame id="comments_article_1"></turbo-frame>
|
38
38
|
def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
|
39
|
-
id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.
|
39
|
+
id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
|
40
40
|
src = url_for(src) if src.present?
|
41
41
|
|
42
42
|
tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
|
@@ -22,9 +22,8 @@ module Turbo::Streams::ActionHelper
|
|
22
22
|
# message = Message.find(1)
|
23
23
|
# turbo_stream_action_tag "remove", target: [message, :special]
|
24
24
|
# # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
|
25
|
-
#
|
26
25
|
def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, **attributes)
|
27
|
-
template = action.to_sym
|
26
|
+
template = action.to_sym.in?(%i[ remove refresh ]) ? "" : tag.template(template.to_s.html_safe)
|
28
27
|
|
29
28
|
if target = convert_to_turbo_stream_dom_id(target)
|
30
29
|
tag.turbo_stream(template, **attributes, action: action, target: target)
|
@@ -35,10 +34,19 @@ module Turbo::Streams::ActionHelper
|
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
37
|
+
# Creates a `turbo-stream` tag with an `action="refresh"` attribute. Example:
|
38
|
+
#
|
39
|
+
# turbo_stream_refresh_tag
|
40
|
+
# # => <turbo-stream action="refresh"></turbo-stream>
|
41
|
+
def turbo_stream_refresh_tag(request_id: Turbo.current_request_id, **attributes)
|
42
|
+
turbo_stream_action_tag(:refresh, **{ "request-id": request_id }.compact, **attributes)
|
43
|
+
end
|
44
|
+
|
38
45
|
private
|
39
46
|
def convert_to_turbo_stream_dom_id(target, include_selector: false)
|
40
|
-
|
41
|
-
|
47
|
+
target_array = Array.wrap(target)
|
48
|
+
if target_array.any? { |value| value.respond_to?(:to_key) }
|
49
|
+
"#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(*target_array)}"
|
42
50
|
else
|
43
51
|
target
|
44
52
|
end
|
@@ -49,7 +49,12 @@ module Turbo::StreamsHelper
|
|
49
49
|
#
|
50
50
|
# <%= turbo_stream_from "room", channel: RoomChannel, data: {room_name: "room #1"} %>
|
51
51
|
#
|
52
|
+
# Raises an +ArgumentError+ if all streamables are blank
|
53
|
+
#
|
54
|
+
# <%= turbo_stream_from("") %> # => ArgumentError: streamables can't be blank
|
55
|
+
# <%= turbo_stream_from("", nil) %> # => ArgumentError: streamables can't be blank
|
52
56
|
def turbo_stream_from(*streamables, **attributes)
|
57
|
+
raise ArgumentError, "streamables can't be blank" unless streamables.any?(&:present?)
|
53
58
|
attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
|
54
59
|
attributes[:"signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables)
|
55
60
|
|
@@ -3,6 +3,8 @@ import { subscribeTo } from "./cable"
|
|
3
3
|
import snakeize from "./snakeize"
|
4
4
|
|
5
5
|
class TurboCableStreamSourceElement extends HTMLElement {
|
6
|
+
static observedAttributes = ["channel", "signed-stream-name"]
|
7
|
+
|
6
8
|
async connectedCallback() {
|
7
9
|
connectStreamSource(this)
|
8
10
|
this.subscription = await subscribeTo(this.channel, {
|
@@ -15,6 +17,14 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
15
17
|
disconnectedCallback() {
|
16
18
|
disconnectStreamSource(this)
|
17
19
|
if (this.subscription) this.subscription.unsubscribe()
|
20
|
+
this.subscriptionDisconnected()
|
21
|
+
}
|
22
|
+
|
23
|
+
attributeChangedCallback() {
|
24
|
+
if (this.subscription) {
|
25
|
+
this.disconnectedCallback()
|
26
|
+
this.connectedCallback()
|
27
|
+
}
|
18
28
|
}
|
19
29
|
|
20
30
|
dispatchMessageEvent(data) {
|
@@ -2,7 +2,7 @@
|
|
2
2
|
class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base
|
3
3
|
discard_on ActiveJob::DeserializationError
|
4
4
|
|
5
|
-
def perform(stream, action:, target:, **rendering)
|
6
|
-
Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering
|
5
|
+
def perform(stream, action:, target:, attributes: {}, **rendering)
|
6
|
+
Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, attributes: attributes, **rendering
|
7
7
|
end
|
8
8
|
end
|