turbo-rails 1.5.0 → 2.0.7

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -16
  3. data/app/assets/javascripts/turbo.js +1974 -785
  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 +33 -7
  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 +6 -3
  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/index.js +2 -0
  16. data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
  17. data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
  18. data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
  19. data/app/models/concerns/turbo/broadcastable.rb +184 -35
  20. data/app/models/turbo/debouncer.rb +24 -0
  21. data/app/models/turbo/streams/tag_builder.rb +20 -0
  22. data/app/models/turbo/thread_debouncer.rb +28 -0
  23. data/config/routes.rb +3 -4
  24. data/lib/install/turbo_with_importmap.rb +1 -1
  25. data/lib/tasks/turbo_tasks.rake +0 -22
  26. data/lib/turbo/broadcastable/test_helper.rb +5 -5
  27. data/lib/turbo/engine.rb +34 -8
  28. data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
  29. data/lib/turbo/test_assertions.rb +2 -2
  30. data/lib/turbo/version.rb +1 -1
  31. data/lib/turbo-rails.rb +10 -0
  32. metadata +9 -5
  33. data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,4 +1,4 @@
1
- # Turbo streams can be broadcast directly from models that include this module (this is automatically done for Active Records).
1
+ # Turbo streams can be broadcasted directly from models that include this module (this is automatically done for Active Records).
2
2
  # This makes it convenient to execute both synchronous and asynchronous updates, and render directly from callbacks in models
3
3
  # or from controllers or jobs that act on those models. Here's an example:
4
4
  #
@@ -14,8 +14,8 @@
14
14
  # end
15
15
  # end
16
16
  #
17
- # This is an example from [HEY](https://hey.com), and the clearance is the model that drives
18
- # [the screener](https://hey.com/features/the-screener/), which gives users the power to deny first-time senders (petitioners)
17
+ # This is an example from {HEY}[https://hey.com], and the clearance is the model that drives
18
+ # {the screener}[https://hey.com/features/the-screener/], which gives users the power to deny first-time senders (petitioners)
19
19
  # access to their attention (as the examiner). When a new clearance is created upon receipt of an email from a first-time
20
20
  # sender, that'll trigger the call to broadcast_later, which in turn invokes <tt>broadcast_prepend_later_to</tt>.
21
21
  #
@@ -27,7 +27,7 @@
27
27
  # (which is derived by default from the plural model name of the model, but can be overwritten).
28
28
  #
29
29
  # You can also choose to render html instead of a partial inside of a broadcast
30
- # you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
30
+ # you do this by passing the +html:+ option to any broadcast method that accepts the **rendering argument. Example:
31
31
  #
32
32
  # class Message < ApplicationRecord
33
33
  # belongs_to :user
@@ -40,8 +40,8 @@
40
40
  # end
41
41
  # end
42
42
  #
43
- # If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the `template:` option.
44
- # Again, only to any broadcast method that accepts the `**rendering` argument. Example:
43
+ # If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the +template:+ option.
44
+ # Again, only to any broadcast method that accepts the +**rendering+ argument. Example:
45
45
  #
46
46
  # class Message < ApplicationRecord
47
47
  # belongs_to :user
@@ -54,7 +54,7 @@
54
54
  # end
55
55
  # end
56
56
  #
57
- # If you want to render a renderable object you can use the `renderable:` option.
57
+ # If you want to render a renderable object you can use the +renderable:+ option.
58
58
  #
59
59
  # class Message < ApplicationRecord
60
60
  # belongs_to :user
@@ -67,17 +67,99 @@
67
67
  # end
68
68
  # end
69
69
  #
70
- # There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
71
- # <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
70
+ # There are seven basic actions you can broadcast: <tt>after</tt>, <tt>append</tt>, <tt>before</tt>,
71
+ # <tt>prepend</tt>, <tt>remove</tt>, <tt>replace</tt>, and
72
+ # <tt>update</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
72
73
  # within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
73
74
  # execution. You don't need to do this for remove, since only the dom id for the model is used.
74
75
  #
75
- # In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
76
+ # In addition to the seven basic actions, you can also use <tt>broadcast_render</tt>,
76
77
  # <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
77
78
  # to render a turbo stream template with multiple actions.
79
+ #
80
+ # == Page refreshes
81
+ #
82
+ # You can broadcast "page refresh" stream actions. This will make subscribed clients reload the
83
+ # page. For pages that configure morphing and scroll preservation, this will translate into smooth
84
+ # updates when it only updates the content that changed.
85
+ #
86
+ # This approach is an alternative to fine-grained stream actions targeting specific DOM elements. It
87
+ # offers good fidelity with a much simpler programming model. As a tradeoff, the fidelity you can reach
88
+ # is often not as high as with targeted stream actions since it renders the entire page again.
89
+ #
90
+ # The +broadcasts_refreshes+ class method configures the model to broadcast a "page refresh" on creates,
91
+ # updates, and destroys to a stream name derived at runtime by the <tt>stream</tt> symbol invocation. Examples
92
+ #
93
+ # class Board < ApplicationRecord
94
+ # broadcasts_refreshes
95
+ # end
96
+ #
97
+ # In this example, when a board is created, updated, or destroyed, a Turbo Stream for a
98
+ # page refresh will be broadcasted to all clients subscribed to the "boards" stream.
99
+ #
100
+ # This works great in hierarchical structures, where the child record touches parent records automatically
101
+ # to invalidate the cache:
102
+ #
103
+ # class Column < ApplicationRecord
104
+ # belongs_to :board, touch: true # +Board+ will trigger a page refresh on column changes
105
+ # end
106
+ #
107
+ # You can also specify the streamable declaratively by passing a symbol to the +broadcasts_refreshes_to+ method:
108
+ #
109
+ # class Column < ApplicationRecord
110
+ # belongs_to :board
111
+ # broadcasts_refreshes_to :board
112
+ # end
113
+ #
114
+ # For more granular control, you can also broadcast a "page refresh" to a stream name derived
115
+ # from the passed <tt>streamables</tt> by using the instance-level methods <tt>broadcast_refresh_to</tt> or
116
+ # <tt>broadcast_refresh_later_to</tt>. These methods are particularly useful when you want to trigger
117
+ # a page refresh for more specific scenarios. Example:
118
+ #
119
+ # class Clearance < ApplicationRecord
120
+ # belongs_to :petitioner, class_name: "Contact"
121
+ # belongs_to :examiner, class_name: "User"
122
+ #
123
+ # after_create_commit :broadcast_refresh_later
124
+ #
125
+ # private
126
+ # def broadcast_refresh_later
127
+ # broadcast_refresh_later_to examiner.identity, :clearances
128
+ # end
129
+ # end
130
+ #
131
+ # In this example, a "page refresh" is broadcast to the stream named "identity:<identity-id>:clearances"
132
+ # after a new clearance is created. All clients subscribed to this stream will refresh the page to reflect
133
+ # the changes.
134
+ #
135
+ # When broadcasting page refreshes, Turbo will automatically debounce multiple calls in a row to only broadcast the last one.
136
+ # This is meant for scenarios where you process records in mass. Because of the nature of such signals, it makes no sense to
137
+ # broadcast them repeatedly and individually.
138
+ # == Suppressing broadcasts
139
+ #
140
+ # Sometimes, you need to disable broadcasts in certain scenarios. You can use <tt>.suppressing_turbo_broadcasts</tt> to create
141
+ # execution contexts where broadcasts are disabled:
142
+ #
143
+ # class Message < ApplicationRecord
144
+ # after_create_commit :update_message
145
+ #
146
+ # private
147
+ # def update_message
148
+ # broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
149
+ # end
150
+ # end
151
+ #
152
+ # Message.suppressing_turbo_broadcasts do
153
+ # Message.create!(board: board) # This won't broadcast the replace action
154
+ # end
78
155
  module Turbo::Broadcastable
79
156
  extend ActiveSupport::Concern
80
157
 
158
+ included do
159
+ thread_mattr_accessor :suppressed_turbo_broadcasts, instance_accessor: false
160
+ delegate :suppressed_turbo_broadcasts?, to: "self.class"
161
+ end
162
+
81
163
  module ClassMethods
82
164
  # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
83
165
  # <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
@@ -112,10 +194,46 @@ module Turbo::Broadcastable
112
194
  after_destroy_commit -> { broadcast_remove }
113
195
  end
114
196
 
197
+ # Configures the model to broadcast a "page refresh" on creates, updates, and destroys to a stream
198
+ # name derived at runtime by the <tt>stream</tt> symbol invocation. Examples:
199
+ #
200
+ # class Message < ApplicationRecord
201
+ # belongs_to :board
202
+ # broadcasts_refreshes_to :board
203
+ # end
204
+ #
205
+ # class Message < ApplicationRecord
206
+ # belongs_to :board
207
+ # broadcasts_refreshes_to ->(message) { [ message.board, :messages ] }
208
+ # end
209
+ def broadcasts_refreshes_to(stream)
210
+ after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
211
+ end
212
+
213
+ # Same as <tt>#broadcasts_refreshes_to</tt>, but the designated stream for page refreshes is automatically set to
214
+ # the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
215
+ def broadcasts_refreshes(stream = model_name.plural)
216
+ after_create_commit -> { broadcast_refresh_later_to(stream) }
217
+ after_update_commit -> { broadcast_refresh_later }
218
+ after_destroy_commit -> { broadcast_refresh }
219
+ end
220
+
115
221
  # All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
116
222
  def broadcast_target_default
117
223
  model_name.plural
118
224
  end
225
+
226
+ # Executes +block+ preventing both synchronous and asynchronous broadcasts from this model.
227
+ def suppressing_turbo_broadcasts(&block)
228
+ original, self.suppressed_turbo_broadcasts = self.suppressed_turbo_broadcasts, true
229
+ yield
230
+ ensure
231
+ self.suppressed_turbo_broadcasts = original
232
+ end
233
+
234
+ def suppressed_turbo_broadcasts?
235
+ suppressed_turbo_broadcasts
236
+ end
119
237
  end
120
238
 
121
239
  # Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
@@ -124,7 +242,7 @@ module Turbo::Broadcastable
124
242
  # # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
125
243
  # clearance.broadcast_remove_to examiner.identity, :clearances
126
244
  def broadcast_remove_to(*streamables, target: self)
127
- Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target)
245
+ Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
128
246
  end
129
247
 
130
248
  # Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
@@ -142,8 +260,12 @@ module Turbo::Broadcastable
142
260
  # # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
143
261
  # # to the stream named "identity:2:clearances"
144
262
  # clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
263
+ #
264
+ # # Sends <turbo-stream action="replace" method="morph" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
265
+ # # to the stream named "identity:2:clearances"
266
+ # clearance.broadcast_replace_to examiner.identity, :clearance, attributes: { method: :morph }, partial: "clearances/other_partial", locals: { a: 1 }
145
267
  def broadcast_replace_to(*streamables, **rendering)
146
- Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
268
+ Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
147
269
  end
148
270
 
149
271
  # Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
@@ -161,8 +283,12 @@ module Turbo::Broadcastable
161
283
  # # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
162
284
  # # to the stream named "identity:2:clearances"
163
285
  # clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
286
+ #
287
+ # # sends <turbo-stream action="update" method="morph" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
288
+ # # to the stream named "identity:2:clearances"
289
+ # # clearance.broadcast_update_to examiner.identity, :clearances, attributes: { method: :morph }, partial: "clearances/other_partial", locals: { a: 1 }
164
290
  def broadcast_update_to(*streamables, **rendering)
165
- Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
291
+ Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
166
292
  end
167
293
 
168
294
  # Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
@@ -215,7 +341,7 @@ module Turbo::Broadcastable
215
341
  # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
216
342
  # partial: "clearances/other_partial", locals: { a: 1 }
217
343
  def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
218
- Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
344
+ Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
219
345
  end
220
346
 
221
347
  # Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
@@ -236,7 +362,7 @@ module Turbo::Broadcastable
236
362
  # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
237
363
  # partial: "clearances/other_partial", locals: { a: 1 }
238
364
  def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
239
- Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
365
+ Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
240
366
  end
241
367
 
242
368
  # Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
@@ -244,24 +370,36 @@ module Turbo::Broadcastable
244
370
  broadcast_prepend_to self, target: target, **rendering
245
371
  end
246
372
 
373
+ # Broadcast a "page refresh" to the stream name identified by the passed <tt>streamables</tt>. Example:
374
+ #
375
+ # # Sends <turbo-stream action="refresh"></turbo-stream> to the stream named "identity:2:clearances"
376
+ # clearance.broadcast_refresh_to examiner.identity, :clearances
377
+ def broadcast_refresh_to(*streamables)
378
+ Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
379
+ end
380
+
381
+ # Same as <tt>#broadcast_refresh_to</tt>, but the designated stream is automatically set to the current model.
382
+ def broadcast_refresh
383
+ broadcast_refresh_to self
384
+ end
385
+
247
386
  # Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
248
387
  #
249
388
  # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
250
389
  # # to the stream named "identity:2:clearances"
251
390
  # 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))
391
+ def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
392
+ Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
254
393
  end
255
394
 
256
395
  # 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
396
+ def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering)
397
+ broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering
259
398
  end
260
399
 
261
-
262
400
  # Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
263
401
  def broadcast_replace_later_to(*streamables, **rendering)
264
- Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
402
+ Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
265
403
  end
266
404
 
267
405
  # Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -271,7 +409,7 @@ module Turbo::Broadcastable
271
409
 
272
410
  # Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
273
411
  def broadcast_update_later_to(*streamables, **rendering)
274
- Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
412
+ Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
275
413
  end
276
414
 
277
415
  # Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -281,7 +419,7 @@ module Turbo::Broadcastable
281
419
 
282
420
  # Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
283
421
  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))
422
+ Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
285
423
  end
286
424
 
287
425
  # Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -291,7 +429,7 @@ module Turbo::Broadcastable
291
429
 
292
430
  # Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
293
431
  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))
432
+ Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
295
433
  end
296
434
 
297
435
  # Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -299,14 +437,24 @@ module Turbo::Broadcastable
299
437
  broadcast_prepend_later_to self, target: target, **rendering
300
438
  end
301
439
 
440
+ # Same as <tt>broadcast_refresh_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
441
+ def broadcast_refresh_later_to(*streamables)
442
+ Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
443
+ end
444
+
445
+ # Same as <tt>#broadcast_refresh_later_to</tt>, but the designated stream is automatically set to the current model.
446
+ def broadcast_refresh_later
447
+ broadcast_refresh_later_to self
448
+ end
449
+
302
450
  # 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))
451
+ def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
452
+ Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
305
453
  end
306
454
 
307
455
  # 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
456
+ def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering)
457
+ broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering
310
458
  end
311
459
 
312
460
  # Render a turbo stream template with this broadcastable model passed as the local variable. Example:
@@ -325,7 +473,7 @@ module Turbo::Broadcastable
325
473
  #
326
474
  # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
327
475
  # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
328
- # be using `broadcast_render_later`, unless you specifically know why synchronous rendering is needed.
476
+ # be using +broadcast_render_later+, unless you specifically know why synchronous rendering is needed.
329
477
  def broadcast_render(**rendering)
330
478
  broadcast_render_to self, **rendering
331
479
  end
@@ -335,12 +483,12 @@ module Turbo::Broadcastable
335
483
  #
336
484
  # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
337
485
  # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
338
- # be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
486
+ # be using +broadcast_render_later_to+, unless you specifically know why synchronous rendering is needed.
339
487
  def broadcast_render_to(*streamables, **rendering)
340
- Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
488
+ Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
341
489
  end
342
490
 
343
- # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
491
+ # Same as <tt>broadcast_render_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
344
492
  def broadcast_render_later(**rendering)
345
493
  broadcast_render_later_to self, **rendering
346
494
  end
@@ -348,10 +496,9 @@ module Turbo::Broadcastable
348
496
  # Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
349
497
  # <tt>streamables</tt>.
350
498
  def broadcast_render_later_to(*streamables, **rendering)
351
- Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering))
499
+ Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
352
500
  end
353
501
 
354
-
355
502
  private
356
503
  def broadcast_target_default
357
504
  self.class.broadcast_target_default
@@ -361,12 +508,14 @@ module Turbo::Broadcastable
361
508
  options.tap do |o|
362
509
  # Add the current instance into the locals with the element name (which is the un-namespaced name)
363
510
  # 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)
511
+ o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
365
512
 
366
513
  if o[:html] || o[:partial]
367
514
  return o
368
515
  elsif o[:template] || o[:renderable]
369
516
  o[:layout] = false
517
+ elsif o[:render] == false
518
+ return o
370
519
  else
371
520
  # if none of these options are passed in, it will set a partial from #to_partial_path
372
521
  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,6 +1,5 @@
1
- # FIXME: Offer flag to opt out of these native routes
2
1
  Rails.application.routes.draw do
3
- get "recede_historical_location" => "turbo/native/navigation#recede", as: :turbo_recede_historical_location
4
- get "resume_historical_location" => "turbo/native/navigation#resume", as: :turbo_resume_historical_location
5
- get "refresh_historical_location" => "turbo/native/navigation#refresh", as: :turbo_refresh_historical_location
2
+ get "recede_historical_location", to: "turbo/native/navigation#recede", as: :turbo_recede_historical_location
3
+ get "resume_historical_location", to: "turbo/native/navigation#resume", as: :turbo_resume_historical_location
4
+ get "refresh_historical_location", to: "turbo/native/navigation#refresh", as: :turbo_refresh_historical_location
6
5
  end if Turbo.draw_routes
@@ -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)
@@ -5,20 +5,6 @@ module Turbo
5
5
  system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
6
6
  end
7
7
 
8
- def redis_installed?
9
- Gem.win_platform? ?
10
- system('where redis-server > NUL 2>&1') :
11
- system('which redis-server > /dev/null')
12
- end
13
-
14
- def switch_on_redis_if_available
15
- if redis_installed?
16
- Rake::Task["turbo:install:redis"].invoke
17
- else
18
- puts "Run turbo:install:redis to switch on Redis and use it in development for turbo streams"
19
- end
20
- end
21
-
22
8
  def using_bun?
23
9
  Rails.root.join("bun.config.js").exist?
24
10
  end
@@ -43,24 +29,16 @@ namespace :turbo do
43
29
  desc "Install Turbo into the app with asset pipeline"
44
30
  task :importmap do
45
31
  Turbo::Tasks.run_turbo_install_template "turbo_with_importmap"
46
- Turbo::Tasks.switch_on_redis_if_available
47
32
  end
48
33
 
49
34
  desc "Install Turbo into the app with webpacker"
50
35
  task :node do
51
36
  Turbo::Tasks.run_turbo_install_template "turbo_with_node"
52
- Turbo::Tasks.switch_on_redis_if_available
53
37
  end
54
38
 
55
39
  desc "Install Turbo into the app with bun"
56
40
  task :bun do
57
41
  Turbo::Tasks.run_turbo_install_template "turbo_with_bun"
58
- Turbo::Tasks.switch_on_redis_if_available
59
- end
60
-
61
- desc "Switch on Redis and use it in development"
62
- task :redis do
63
- Turbo::Tasks.run_turbo_install_template "turbo_needs_redis"
64
42
  end
65
43
  end
66
44
  end
@@ -11,14 +11,14 @@ module Turbo
11
11
 
12
12
  # Asserts that `<turbo-stream>` elements were broadcast over Action Cable
13
13
  #
14
- # === Arguments
14
+ # ==== Arguments
15
15
  #
16
16
  # * <tt>stream_name_or_object</tt> the objects used to generate the
17
17
  # channel Action Cable name, or the name itself
18
18
  # * <tt>&block</tt> optional block executed before the
19
19
  # assertion
20
20
  #
21
- # === Options
21
+ # ==== Options
22
22
  #
23
23
  # * <tt>count:</tt> the number of `<turbo-stream>` elements that are
24
24
  # expected to be broadcast
@@ -64,13 +64,13 @@ 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
 
71
71
  # Asserts that no `<turbo-stream>` elements were broadcast over Action Cable
72
72
  #
73
- # === Arguments
73
+ # ==== Arguments
74
74
  #
75
75
  # * <tt>stream_name_or_object</tt> the objects used to generate the
76
76
  # channel Action Cable name, or the name itself
@@ -113,7 +113,7 @@ module Turbo
113
113
 
114
114
  # Captures any `<turbo-stream>` elements that were broadcast over Action Cable
115
115
  #
116
- # === Arguments
116
+ # ==== Arguments
117
117
  #
118
118
  # * <tt>stream_name_or_object</tt> the objects used to generate the
119
119
  # channel Action Cable name, or the name itself
data/lib/turbo/engine.rb CHANGED
@@ -15,8 +15,25 @@ module Turbo
15
15
  #{root}/app/jobs
16
16
  )
17
17
 
18
+ # If the parent application does not use Action Cable, app/channels cannot
19
+ # be eager loaded, because it references the ActionCable constant.
20
+ #
21
+ # When turbo-rails depends on Rails 7 or above, the entire block can be
22
+ # reduced to
23
+ #
24
+ # unless defined?(ActionCable)
25
+ # Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
26
+ # end
27
+ #
18
28
  initializer "turbo.no_action_cable", before: :set_eager_load_paths do
19
- config.eager_load_paths.delete("#{root}/app/channels") unless defined?(ActionCable)
29
+ unless defined?(ActionCable)
30
+ if Rails.autoloaders.zeitwerk_enabled?
31
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
32
+ else
33
+ # This else branch only runs in Rails 6.x + classic mode.
34
+ config.eager_load_paths.delete("#{root}/app/channels")
35
+ end
36
+ end
20
37
  end
21
38
 
22
39
  # If you don't want to precompile Turbo's assets (eg. because you're using webpack),
@@ -46,6 +63,12 @@ module Turbo
46
63
  end
47
64
  end
48
65
 
66
+ initializer "turbo.request_id_tracking" do
67
+ ActiveSupport.on_load(:action_controller) do
68
+ include Turbo::RequestIdTracking
69
+ end
70
+ end
71
+
49
72
  initializer "turbo.broadcastable" do
50
73
  ActiveSupport.on_load(:active_record) do
51
74
  include Turbo::Broadcastable
@@ -57,11 +80,9 @@ module Turbo
57
80
  end
58
81
 
59
82
  initializer "turbo.renderer" do
60
- ActiveSupport.on_load(:action_controller) do
61
- ActionController::Renderers.add :turbo_stream do |turbo_streams_html, options|
62
- self.content_type = Mime[:turbo_stream] if media_type.nil?
63
- turbo_streams_html
64
- end
83
+ ActionController::Renderers.add :turbo_stream do |turbo_streams_html, options|
84
+ self.content_type = Mime[:turbo_stream] if media_type.nil?
85
+ turbo_streams_html
65
86
  end
66
87
  end
67
88
 
@@ -75,11 +96,16 @@ module Turbo
75
96
  initializer "turbo.test_assertions" do
76
97
  ActiveSupport.on_load(:active_support_test_case) do
77
98
  require "turbo/test_assertions"
78
- require "turbo/broadcastable/test_helper"
79
-
80
99
  include Turbo::TestAssertions
81
100
  end
82
101
 
102
+ ActiveSupport.on_load(:action_cable) do
103
+ ActiveSupport.on_load(:active_support_test_case) do
104
+ require "turbo/broadcastable/test_helper"
105
+ include Turbo::Broadcastable::TestHelper
106
+ end
107
+ end
108
+
83
109
  ActiveSupport.on_load(:action_dispatch_integration_test) do
84
110
  require "turbo/test_assertions/integration_test_assertions"
85
111