turbo-rails 1.5.0 → 2.0.7

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