turbo-rails 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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