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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -16
  3. data/app/assets/javascripts/turbo.js +2226 -953
  4. data/app/assets/javascripts/turbo.min.js +9 -5
  5. data/app/assets/javascripts/turbo.min.js.map +1 -1
  6. data/app/channels/turbo/streams/broadcasts.rb +47 -10
  7. data/app/channels/turbo/streams_channel.rb +15 -15
  8. data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
  9. data/app/controllers/turbo/frames/frame_request.rb +2 -2
  10. data/app/controllers/turbo/native/navigation.rb +17 -11
  11. data/app/helpers/turbo/drive_helper.rb +72 -14
  12. data/app/helpers/turbo/frames_helper.rb +8 -8
  13. data/app/helpers/turbo/streams/action_helper.rb +12 -4
  14. data/app/helpers/turbo/streams_helper.rb +5 -0
  15. data/app/javascript/turbo/cable_stream_source_element.js +10 -0
  16. data/app/javascript/turbo/index.js +2 -0
  17. data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
  18. data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
  19. data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
  20. data/app/models/concerns/turbo/broadcastable.rb +201 -42
  21. data/app/models/turbo/debouncer.rb +24 -0
  22. data/app/models/turbo/streams/tag_builder.rb +50 -12
  23. data/app/models/turbo/thread_debouncer.rb +28 -0
  24. data/config/routes.rb +3 -4
  25. data/lib/install/turbo_with_importmap.rb +1 -1
  26. data/lib/tasks/turbo_tasks.rake +0 -22
  27. data/lib/turbo/broadcastable/test_helper.rb +5 -5
  28. data/lib/turbo/engine.rb +80 -9
  29. data/lib/turbo/system_test_helper.rb +128 -0
  30. data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
  31. data/lib/turbo/test_assertions.rb +2 -2
  32. data/lib/turbo/version.rb +1 -1
  33. data/lib/turbo-rails.rb +10 -0
  34. metadata +10 -19
  35. data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,4 +1,4 @@
1
- # Provides the broadcast actions in synchronous and asynchrous form for the <tt>Turbo::StreamsChannel</tt>.
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 broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
37
- broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
38
- rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil)
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 broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering)
67
- Turbo::Streams::ActionBroadcastJob.perform_later \
68
- stream_name_from(streamables), action: action, target: target, targets: targets, **rendering
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
- ActionCable.server.broadcast stream_name_from(streamables), content
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
- # extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
13
- # include Turbo::Streams::StreamName::ClassMethods
12
+ # extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
13
+ # include Turbo::Streams::StreamName::ClassMethods
14
14
  #
15
- # def subscribed
16
- # if (stream_name = verified_stream_name_from_params).present? &&
15
+ # def subscribed
16
+ # if (stream_name = verified_stream_name_from_params).present? &&
17
17
  # subscription_allowed?
18
- # stream_from stream_name
19
- # else
20
- # reject
21
- # end
22
- # end
18
+ # stream_from stream_name
19
+ # else
20
+ # reject
21
+ # end
22
+ # end
23
23
  #
24
- # def subscription_allowed?
25
- # # ...
26
- # end
24
+ # def subscription_allowed?
25
+ # # ...
26
+ # end
27
27
  # end
28
28
  #
29
- # This channel can be connected to a web page using <tt>:channel</tt> option in
30
- # <tt>turbo_stream_from</tt> helper:
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
- # <%= turbo_stream_from 'room', channel: CustomChannel %>
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<tt>.
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 Turbo Native clients running (see the Turbo iOS and Turbo Android projects for details), you can respond to native
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
- # turbo-android handles these actions automatically. You are required to implement the handling on your own for turbo-ios.
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
- # Turbo Native applications are identified by having the string "Turbo Native" as part of their user agent.
14
- def turbo_native_app?
15
- request.user_agent.to_s.match?(/Turbo Native/)
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
- # Tell the Turbo Native app to dismiss a modal (if presented) or pop a screen off of the navigation stack.
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, tag.meta(name: "turbo-cache-control", content: "no-cache")
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, tag.meta(name: "turbo-cache-control", content: "no-preview")
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, tag.meta(name: "turbo-visit-control", content: "reload")
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
- # === Examples
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
- # The `turbo_frame_tag` helper will convert the arguments it receives to their
28
- # `dom_id` if applicable to easily generate unique ids for Turbo Frames:
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="article_1_comments"></turbo-frame>
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.first
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 == :remove ? "" : tag.template(template.to_s.html_safe)
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
- if Array(target).any? { |value| value.respond_to?(:to_key) }
41
- "#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(*target)}"
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) {
@@ -8,4 +8,6 @@ export { cable }
8
8
 
9
9
  import { encodeMethodIntoRequestBody } from "./fetch_requests"
10
10
 
11
+ window.Turbo = Turbo
12
+
11
13
  addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)
@@ -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
@@ -2,7 +2,7 @@
2
2
  # turbo stream templates.
3
3
  class Turbo::Streams::BroadcastJob < ActiveJob::Base
4
4
  discard_on ActiveJob::DeserializationError
5
-
5
+
6
6
  def perform(stream, **rendering)
7
7
  Turbo::StreamsChannel.broadcast_render_to stream, **rendering
8
8
  end
@@ -0,0 +1,7 @@
1
+ class Turbo::Streams::BroadcastStreamJob < ActiveJob::Base
2
+ discard_on ActiveJob::DeserializationError
3
+
4
+ def perform(stream, content:)
5
+ Turbo::StreamsChannel.broadcast_stream_to(stream, content: content)
6
+ end
7
+ end