turbo-rails 1.5.0 → 2.0.0

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.
@@ -33,9 +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)
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)
37
41
  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)
42
+ rendering.delete(:content) || rendering.delete(:html) || (rendering[:render] != false && rendering.any? ? render_format(:html, **rendering) : nil),
43
+ **attributes
39
44
  ))
40
45
  end
41
46
 
@@ -63,9 +68,15 @@ module Turbo::Streams::Broadcasts
63
68
  broadcast_action_later_to(*streamables, action: :prepend, **opts)
64
69
  end
65
70
 
66
- def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering)
71
+ def broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **opts)
72
+ refresh_debouncer_for(*streamables, request_id: request_id).debounce do
73
+ Turbo::Streams::BroadcastStreamJob.perform_later stream_name_from(streamables), content: turbo_stream_refresh_tag(request_id: request_id, **opts)
74
+ end
75
+ end
76
+
77
+ def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
67
78
  Turbo::Streams::ActionBroadcastJob.perform_later \
68
- stream_name_from(streamables), action: action, target: target, targets: targets, **rendering
79
+ stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
69
80
  end
70
81
 
71
82
  def broadcast_render_to(*streamables, **rendering)
@@ -80,6 +91,9 @@ module Turbo::Streams::Broadcasts
80
91
  ActionCable.server.broadcast stream_name_from(streamables), content
81
92
  end
82
93
 
94
+ def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
95
+ Turbo::ThreadDebouncer.for("turbo-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
96
+ end
83
97
 
84
98
  private
85
99
  def render_format(format, **rendering)
@@ -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
@@ -1,5 +1,9 @@
1
1
  module Turbo::DriveHelper
2
- # Note: These helpers require a +yield :head+ provision in the layout.
2
+ # Helpers to configure Turbo Drive via meta directives. They come in two
3
+ # variants:
4
+ #
5
+ # The recommended option is to include +yield :head+ in the +<head>+ section
6
+ # of the layout. Then you can use the helpers in any view.
3
7
  #
4
8
  # ==== Example
5
9
  #
@@ -9,21 +13,76 @@ module Turbo::DriveHelper
9
13
  # # app/views/trays/index.html.erb
10
14
  # <% turbo_exempts_page_from_cache %>
11
15
  # <p>Page that shouldn't be cached by Turbo</p>
16
+ #
17
+ # Alternatively, you can use the +_tag+ variant of the helpers to only get the
18
+ # HTML for the meta directive.
12
19
 
13
20
  # Pages that are more likely than not to be a cache miss can skip turbo cache to avoid visual jitter.
14
21
  # Cannot be used along with +turbo_exempts_page_from_preview+.
15
22
  def turbo_exempts_page_from_cache
16
- provide :head, tag.meta(name: "turbo-cache-control", content: "no-cache")
23
+ provide :head, turbo_exempts_page_from_cache_tag
24
+ end
25
+
26
+ # See +turbo_exempts_page_from_cache+.
27
+ def turbo_exempts_page_from_cache_tag
28
+ tag.meta(name: "turbo-cache-control", content: "no-cache")
17
29
  end
18
30
 
19
31
  # Specify that a cached version of the page should not be shown as a preview during an application visit.
20
32
  # Cannot be used along with +turbo_exempts_page_from_cache+.
21
33
  def turbo_exempts_page_from_preview
22
- provide :head, tag.meta(name: "turbo-cache-control", content: "no-preview")
34
+ provide :head, turbo_exempts_page_from_preview_tag
35
+ end
36
+
37
+ # See +turbo_exempts_page_from_preview+.
38
+ def turbo_exempts_page_from_preview_tag
39
+ tag.meta(name: "turbo-cache-control", content: "no-preview")
23
40
  end
24
41
 
25
42
  # Force the page, when loaded by Turbo, to be cause a full page reload.
26
43
  def turbo_page_requires_reload
27
- provide :head, tag.meta(name: "turbo-visit-control", content: "reload")
44
+ provide :head, turbo_page_requires_reload_tag
45
+ end
46
+
47
+ # See +turbo_page_requires_reload+.
48
+ def turbo_page_requires_reload_tag
49
+ tag.meta(name: "turbo-visit-control", content: "reload")
50
+ end
51
+
52
+ # Configure how to handle page refreshes. A page refresh happens when
53
+ # Turbo loads the current page again with a *replace* visit:
54
+ #
55
+ # === Parameters:
56
+ #
57
+ # * <tt>method</tt> - Method to update the +<body>+ of the page
58
+ # during a page refresh. It can be one of:
59
+ # * +replace:+: Replaces the existing +<body>+ with the new one. This is the
60
+ # default behavior.
61
+ # * +morph:+: Morphs the existing +<body>+ into the new one.
62
+ #
63
+ # * <tt>scroll</tt> - Controls the scroll behavior when a page refresh happens. It
64
+ # can be one of:
65
+ # * +reset:+: Resets scroll to the top, left corner. This is the default.
66
+ # * +preserve:+: Keeps the scroll.
67
+ #
68
+ # === Example Usage:
69
+ #
70
+ # turbo_refreshes_with(method: :morph, scroll: :preserve)
71
+ def turbo_refreshes_with(method: :replace, scroll: :reset)
72
+ provide :head, turbo_refresh_method_tag(method)
73
+ provide :head, turbo_refresh_scroll_tag(scroll)
74
+ end
75
+
76
+ # Configure method to perform page refreshes. See +turbo_refreshes_with+.
77
+ def turbo_refresh_method_tag(method = :replace)
78
+ raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ])
79
+ tag.meta(name: "turbo-refresh-method", content: method)
80
+ end
81
+
82
+ # Configure scroll strategy for page refreshes. See +turbo_refreshes_with+.
83
+ def turbo_refresh_scroll_tag(scroll = :reset)
84
+ raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ])
85
+ tag.meta(name: "turbo-refresh-scroll", content: scroll)
28
86
  end
29
87
  end
88
+
@@ -24,6 +24,9 @@ module Turbo::FramesHelper
24
24
  # <% end %>
25
25
  # # => <turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
26
26
  #
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
+ #
27
30
  # The `turbo_frame_tag` helper will convert the arguments it receives to their
28
31
  # `dom_id` if applicable to easily generate unique ids for Turbo Frames:
29
32
  #
@@ -36,7 +39,7 @@ module Turbo::FramesHelper
36
39
  # <%= turbo_frame_tag(Article.find(1), Comment.new) %>
37
40
  # # => <turbo-frame id="article_1_new_comment"></turbo-frame>
38
41
  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
42
+ id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
40
43
  src = url_for(src) if src.present?
41
44
 
42
45
  tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
@@ -24,7 +24,7 @@ module Turbo::Streams::ActionHelper
24
24
  # # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
25
25
  #
26
26
  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)
27
+ template = action.to_sym.in?(%i[ remove refresh ]) ? "" : tag.template(template.to_s.html_safe)
28
28
 
29
29
  if target = convert_to_turbo_stream_dom_id(target)
30
30
  tag.turbo_stream(template, **attributes, action: action, target: target)
@@ -35,6 +35,10 @@ module Turbo::Streams::ActionHelper
35
35
  end
36
36
  end
37
37
 
38
+ def turbo_stream_refresh_tag(request_id: Turbo.current_request_id, **attributes)
39
+ turbo_stream_action_tag(:refresh, **{ "request-id": request_id }.compact, **attributes)
40
+ end
41
+
38
42
  private
39
43
  def convert_to_turbo_stream_dom_id(target, include_selector: false)
40
44
  if Array(target).any? { |value| value.respond_to?(:to_key) }
@@ -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
@@ -75,9 +75,32 @@
75
75
  # In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
76
76
  # <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
77
77
  # to render a turbo stream template with multiple actions.
78
+ #
79
+ # == Suppressing broadcasts
80
+ #
81
+ # Sometimes, you need to disable broadcasts in certain scenarios. You can use <tt>.suppressing_turbo_broadcasts</tt> to create
82
+ # execution contexts where broadcasts are disabled:
83
+ #
84
+ # class Message < ApplicationRecord
85
+ # after_create_commit :update_message
86
+ #
87
+ # private
88
+ # def update_message
89
+ # broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
90
+ # end
91
+ # end
92
+ #
93
+ # Message.suppressing_turbo_broadcasts do
94
+ # Message.create!(board: board) # This won't broadcast the replace action
95
+ # end
78
96
  module Turbo::Broadcastable
79
97
  extend ActiveSupport::Concern
80
98
 
99
+ included do
100
+ thread_mattr_accessor :suppressed_turbo_broadcasts, instance_accessor: false
101
+ delegate :suppressed_turbo_broadcasts?, to: "self.class"
102
+ end
103
+
81
104
  module ClassMethods
82
105
  # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
83
106
  # <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
@@ -112,10 +135,36 @@ module Turbo::Broadcastable
112
135
  after_destroy_commit -> { broadcast_remove }
113
136
  end
114
137
 
138
+ # Configures the model to broadcast a "page refresh" on creates, updates, and destroys to a stream
139
+ # name derived at runtime by the <tt>stream</tt> symbol invocation.
140
+ def broadcasts_refreshes_to(stream)
141
+ after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
142
+ end
143
+
144
+ # Same as <tt>#broadcasts_refreshes_to</tt>, but the designated stream for page refreshes is automatically set to
145
+ # the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
146
+ def broadcasts_refreshes(stream = model_name.plural)
147
+ after_create_commit -> { broadcast_refresh_later_to(stream) }
148
+ after_update_commit -> { broadcast_refresh_later }
149
+ after_destroy_commit -> { broadcast_refresh }
150
+ end
151
+
115
152
  # All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
116
153
  def broadcast_target_default
117
154
  model_name.plural
118
155
  end
156
+
157
+ # Executes +block+ preventing both synchronous and asynchronous broadcasts from this model.
158
+ def suppressing_turbo_broadcasts(&block)
159
+ original, self.suppressed_turbo_broadcasts = self.suppressed_turbo_broadcasts, true
160
+ yield
161
+ ensure
162
+ self.suppressed_turbo_broadcasts = original
163
+ end
164
+
165
+ def suppressed_turbo_broadcasts?
166
+ suppressed_turbo_broadcasts
167
+ end
119
168
  end
120
169
 
121
170
  # Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
@@ -124,7 +173,7 @@ module Turbo::Broadcastable
124
173
  # # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
125
174
  # clearance.broadcast_remove_to examiner.identity, :clearances
126
175
  def broadcast_remove_to(*streamables, target: self)
127
- Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target)
176
+ Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
128
177
  end
129
178
 
130
179
  # Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
@@ -143,7 +192,7 @@ module Turbo::Broadcastable
143
192
  # # to the stream named "identity:2:clearances"
144
193
  # clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
145
194
  def broadcast_replace_to(*streamables, **rendering)
146
- Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
195
+ Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
147
196
  end
148
197
 
149
198
  # Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
@@ -162,7 +211,7 @@ module Turbo::Broadcastable
162
211
  # # to the stream named "identity:2:clearances"
163
212
  # clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
164
213
  def broadcast_update_to(*streamables, **rendering)
165
- Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
214
+ Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
166
215
  end
167
216
 
168
217
  # Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
@@ -215,7 +264,7 @@ module Turbo::Broadcastable
215
264
  # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
216
265
  # partial: "clearances/other_partial", locals: { a: 1 }
217
266
  def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
218
- Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
267
+ Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
219
268
  end
220
269
 
221
270
  # Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
@@ -236,7 +285,7 @@ module Turbo::Broadcastable
236
285
  # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
237
286
  # partial: "clearances/other_partial", locals: { a: 1 }
238
287
  def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
239
- Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
288
+ Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
240
289
  end
241
290
 
242
291
  # Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
@@ -244,24 +293,32 @@ module Turbo::Broadcastable
244
293
  broadcast_prepend_to self, target: target, **rendering
245
294
  end
246
295
 
296
+ def broadcast_refresh_to(*streamables)
297
+ Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
298
+ end
299
+
300
+ def broadcast_refresh
301
+ broadcast_refresh_to self
302
+ end
303
+
247
304
  # Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
248
305
  #
249
306
  # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
250
307
  # # to the stream named "identity:2:clearances"
251
308
  # clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
252
- def broadcast_action_to(*streamables, action:, target: broadcast_target_default, **rendering)
253
- Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
309
+ def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
310
+ Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
254
311
  end
255
312
 
256
313
  # Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
257
- def broadcast_action(action, target: broadcast_target_default, **rendering)
258
- broadcast_action_to self, action: action, target: target, **rendering
314
+ def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering)
315
+ broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering
259
316
  end
260
317
 
261
318
 
262
319
  # Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
263
320
  def broadcast_replace_later_to(*streamables, **rendering)
264
- Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
321
+ Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
265
322
  end
266
323
 
267
324
  # Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -271,7 +328,7 @@ module Turbo::Broadcastable
271
328
 
272
329
  # Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
273
330
  def broadcast_update_later_to(*streamables, **rendering)
274
- Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
331
+ Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
275
332
  end
276
333
 
277
334
  # Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -281,7 +338,7 @@ module Turbo::Broadcastable
281
338
 
282
339
  # Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
283
340
  def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
284
- Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
341
+ Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
285
342
  end
286
343
 
287
344
  # Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -291,7 +348,7 @@ module Turbo::Broadcastable
291
348
 
292
349
  # Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
293
350
  def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering)
294
- Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
351
+ Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
295
352
  end
296
353
 
297
354
  # Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -299,14 +356,22 @@ module Turbo::Broadcastable
299
356
  broadcast_prepend_later_to self, target: target, **rendering
300
357
  end
301
358
 
359
+ def broadcast_refresh_later_to(*streamables)
360
+ Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
361
+ end
362
+
363
+ def broadcast_refresh_later
364
+ broadcast_refresh_later_to self
365
+ end
366
+
302
367
  # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
303
- def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, **rendering)
304
- Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
368
+ def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
369
+ Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
305
370
  end
306
371
 
307
372
  # Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
308
- def broadcast_action_later(action:, target: broadcast_target_default, **rendering)
309
- broadcast_action_later_to self, action: action, target: target, **rendering
373
+ def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering)
374
+ broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering
310
375
  end
311
376
 
312
377
  # Render a turbo stream template with this broadcastable model passed as the local variable. Example:
@@ -337,7 +402,7 @@ module Turbo::Broadcastable
337
402
  # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
338
403
  # be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
339
404
  def broadcast_render_to(*streamables, **rendering)
340
- Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
405
+ Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
341
406
  end
342
407
 
343
408
  # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
@@ -348,7 +413,7 @@ module Turbo::Broadcastable
348
413
  # Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
349
414
  # <tt>streamables</tt>.
350
415
  def broadcast_render_later_to(*streamables, **rendering)
351
- Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering))
416
+ Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
352
417
  end
353
418
 
354
419
 
@@ -361,12 +426,14 @@ module Turbo::Broadcastable
361
426
  options.tap do |o|
362
427
  # Add the current instance into the locals with the element name (which is the un-namespaced name)
363
428
  # as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
364
- o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
429
+ o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
365
430
 
366
431
  if o[:html] || o[:partial]
367
432
  return o
368
433
  elsif o[:template] || o[:renderable]
369
434
  o[:layout] = false
435
+ elsif o[:render] == false
436
+ return o
370
437
  else
371
438
  # if none of these options are passed in, it will set a partial from #to_partial_path
372
439
  o[:partial] ||= to_partial_path
@@ -0,0 +1,24 @@
1
+ class Turbo::Debouncer
2
+ attr_reader :delay, :scheduled_task
3
+
4
+ DEFAULT_DELAY = 0.5
5
+
6
+ def initialize(delay: DEFAULT_DELAY)
7
+ @delay = delay
8
+ @scheduled_task = nil
9
+ end
10
+
11
+ def debounce(&block)
12
+ scheduled_task&.cancel unless scheduled_task&.complete?
13
+ @scheduled_task = Concurrent::ScheduledTask.execute(delay, &block)
14
+ end
15
+
16
+ def wait
17
+ scheduled_task&.wait(wait_timeout)
18
+ end
19
+
20
+ private
21
+ def wait_timeout
22
+ delay + 1
23
+ end
24
+ end
@@ -22,6 +22,24 @@
22
22
  # <%= turbo_stream.append dom_id(topic_merge) do %>
23
23
  # <%= link_to topic_merge.topic.name, topic_path(topic_merge.topic) %>
24
24
  # <% end %>
25
+ #
26
+ # To integrate with custom actions, extend this class in response to the :turbo_streams_tag_builder load hook:
27
+ #
28
+ # ActiveSupport.on_load :turbo_streams_tag_builder do
29
+ # def highlight(target)
30
+ # action :highlight, target
31
+ # end
32
+ #
33
+ # def highlight_all(targets)
34
+ # action_all :highlight, targets
35
+ # end
36
+ # end
37
+ #
38
+ # turbo_stream.highlight "my-element"
39
+ # # => <turbo-stream action="highlight" target="my-element"><template></template></turbo-stream>
40
+ #
41
+ # turbo_stream.highlight_all ".my-selector"
42
+ # # => <turbo-stream action="highlight" targets=".my-selector"><template></template></turbo-stream>
25
43
  class Turbo::Streams::TagBuilder
26
44
  include Turbo::Streams::ActionHelper
27
45
 
@@ -246,4 +264,6 @@ class Turbo::Streams::TagBuilder
246
264
  @view_context.render(partial: record, formats: :html)
247
265
  end
248
266
  end
267
+
268
+ ActiveSupport.run_load_hooks :turbo_streams_tag_builder, self
249
269
  end
@@ -0,0 +1,28 @@
1
+ # A decorated debouncer that will store instances in the current thread clearing them
2
+ # after the debounced logic triggers.
3
+ class Turbo::ThreadDebouncer
4
+ delegate :wait, to: :debouncer
5
+
6
+ def self.for(key, delay: Turbo::Debouncer::DEFAULT_DELAY)
7
+ Thread.current[key] ||= new(key, Thread.current, delay: delay)
8
+ end
9
+
10
+ private_class_method :new
11
+
12
+ def initialize(key, thread, delay: )
13
+ @key = key
14
+ @debouncer = Turbo::Debouncer.new(delay: delay)
15
+ @thread = thread
16
+ end
17
+
18
+ def debounce
19
+ debouncer.debounce do
20
+ yield.tap do
21
+ thread[key] = nil
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+ attr_reader :key, :debouncer, :thread
28
+ end
data/config/routes.rb CHANGED
@@ -1,4 +1,3 @@
1
- # FIXME: Offer flag to opt out of these native routes
2
1
  Rails.application.routes.draw do
3
2
  get "recede_historical_location" => "turbo/native/navigation#recede", as: :turbo_recede_historical_location
4
3
  get "resume_historical_location" => "turbo/native/navigation#resume", as: :turbo_resume_historical_location
@@ -2,4 +2,4 @@ say "Import Turbo"
2
2
  append_to_file "app/javascript/application.js", %(import "@hotwired/turbo-rails"\n)
3
3
 
4
4
  say "Pin Turbo"
5
- append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true\n)
5
+ append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js"\n)
@@ -64,7 +64,7 @@ module Turbo
64
64
  else
65
65
  broadcasts = "Turbo Stream broadcast".pluralize(count)
66
66
 
67
- assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were none"
67
+ assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were #{payloads.count}"
68
68
  end
69
69
  end
70
70
 
data/lib/turbo/engine.rb CHANGED
@@ -46,6 +46,12 @@ module Turbo
46
46
  end
47
47
  end
48
48
 
49
+ initializer "turbo.request_id_tracking" do
50
+ ActiveSupport.on_load(:action_controller) do
51
+ include Turbo::RequestIdTracking
52
+ end
53
+ end
54
+
49
55
  initializer "turbo.broadcastable" do
50
56
  ActiveSupport.on_load(:active_record) do
51
57
  include Turbo::Broadcastable
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "1.5.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/turbo-rails.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require "turbo/engine"
2
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
2
3
 
3
4
  module Turbo
4
5
  extend ActiveSupport::Autoload
5
6
 
6
7
  mattr_accessor :draw_routes, default: true
7
8
 
9
+ thread_mattr_accessor :current_request_id
10
+
8
11
  class << self
9
12
  attr_writer :signed_stream_verifier_key
10
13
 
@@ -15,5 +18,12 @@ module Turbo
15
18
  def signed_stream_verifier_key
16
19
  @signed_stream_verifier_key or raise ArgumentError, "Turbo requires a signed_stream_verifier_key"
17
20
  end
21
+
22
+ def with_request_id(request_id)
23
+ old_request_id, self.current_request_id = self.current_request_id, request_id
24
+ yield
25
+ ensure
26
+ self.current_request_id = old_request_id
27
+ end
18
28
  end
19
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-10-11 00:00:00.000000000 Z
13
+ date: 2024-02-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activejob
@@ -69,6 +69,7 @@ files:
69
69
  - app/channels/turbo/streams/broadcasts.rb
70
70
  - app/channels/turbo/streams/stream_name.rb
71
71
  - app/channels/turbo/streams_channel.rb
72
+ - app/controllers/concerns/turbo/request_id_tracking.rb
72
73
  - app/controllers/turbo/frames/frame_request.rb
73
74
  - app/controllers/turbo/native/navigation.rb
74
75
  - app/controllers/turbo/native/navigation_controller.rb
@@ -85,8 +86,11 @@ files:
85
86
  - app/javascript/turbo/snakeize.js
86
87
  - app/jobs/turbo/streams/action_broadcast_job.rb
87
88
  - app/jobs/turbo/streams/broadcast_job.rb
89
+ - app/jobs/turbo/streams/broadcast_stream_job.rb
88
90
  - app/models/concerns/turbo/broadcastable.rb
91
+ - app/models/turbo/debouncer.rb
89
92
  - app/models/turbo/streams/tag_builder.rb
93
+ - app/models/turbo/thread_debouncer.rb
90
94
  - app/views/layouts/turbo_rails/frame.html.erb
91
95
  - config/routes.rb
92
96
  - lib/install/turbo_needs_redis.rb