turbo-rails 1.5.0 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -16
  3. data/app/assets/javascripts/turbo.js +2226 -953
  4. data/app/assets/javascripts/turbo.min.js +9 -5
  5. data/app/assets/javascripts/turbo.min.js.map +1 -1
  6. data/app/channels/turbo/streams/broadcasts.rb +47 -10
  7. data/app/channels/turbo/streams_channel.rb +15 -15
  8. data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
  9. data/app/controllers/turbo/frames/frame_request.rb +2 -2
  10. data/app/controllers/turbo/native/navigation.rb +17 -11
  11. data/app/helpers/turbo/drive_helper.rb +72 -14
  12. data/app/helpers/turbo/frames_helper.rb +8 -8
  13. data/app/helpers/turbo/streams/action_helper.rb +12 -4
  14. data/app/helpers/turbo/streams_helper.rb +5 -0
  15. data/app/javascript/turbo/cable_stream_source_element.js +10 -0
  16. data/app/javascript/turbo/index.js +2 -0
  17. data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
  18. data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
  19. data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
  20. data/app/models/concerns/turbo/broadcastable.rb +201 -42
  21. data/app/models/turbo/debouncer.rb +24 -0
  22. data/app/models/turbo/streams/tag_builder.rb +50 -12
  23. data/app/models/turbo/thread_debouncer.rb +28 -0
  24. data/config/routes.rb +3 -4
  25. data/lib/install/turbo_with_importmap.rb +1 -1
  26. data/lib/tasks/turbo_tasks.rake +0 -22
  27. data/lib/turbo/broadcastable/test_helper.rb +5 -5
  28. data/lib/turbo/engine.rb +80 -9
  29. data/lib/turbo/system_test_helper.rb +128 -0
  30. data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
  31. data/lib/turbo/test_assertions.rb +2 -2
  32. data/lib/turbo/version.rb +1 -1
  33. data/lib/turbo-rails.rb +10 -0
  34. metadata +10 -19
  35. data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,4 +1,4 @@
1
- # 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 if ActiveJob is loaded).
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.
@@ -123,13 +241,13 @@ module Turbo::Broadcastable
123
241
  #
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
- def broadcast_remove_to(*streamables, target: self)
127
- Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target)
244
+ def broadcast_remove_to(*streamables, target: self, **rendering)
245
+ Turbo::StreamsChannel.broadcast_remove_to(*streamables, **extract_options_and_add_target(rendering, 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.
131
- def broadcast_remove
132
- broadcast_remove_to self
249
+ def broadcast_remove(**rendering)
250
+ broadcast_remove_to self, **rendering
133
251
  end
134
252
 
135
253
  # Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed
@@ -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, **extract_options_and_add_target(rendering, target: self)) 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, **extract_options_and_add_target(rendering, target: self)) 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.
@@ -182,8 +308,10 @@ module Turbo::Broadcastable
182
308
  # # to the stream named "identity:2:clearances"
183
309
  # clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
184
310
  # partial: "clearances/other_partial", locals: { a: 1 }
185
- def broadcast_before_to(*streamables, target:, **rendering)
186
- Turbo::StreamsChannel.broadcast_before_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
311
+ def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
312
+ raise ArgumentError, "at least one of target or targets is required" unless target || targets
313
+
314
+ Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
187
315
  end
188
316
 
189
317
  # Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
@@ -198,8 +326,10 @@ module Turbo::Broadcastable
198
326
  # # to the stream named "identity:2:clearances"
199
327
  # clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
200
328
  # partial: "clearances/other_partial", locals: { a: 1 }
201
- def broadcast_after_to(*streamables, target:, **rendering)
202
- Turbo::StreamsChannel.broadcast_after_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
329
+ def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
330
+ raise ArgumentError, "at least one of target or targets is required" unless target || targets
331
+
332
+ Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
203
333
  end
204
334
 
205
335
  # Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
@@ -215,7 +345,7 @@ module Turbo::Broadcastable
215
345
  # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
216
346
  # partial: "clearances/other_partial", locals: { a: 1 }
217
347
  def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
218
- Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
348
+ Turbo::StreamsChannel.broadcast_append_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
219
349
  end
220
350
 
221
351
  # Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
@@ -236,7 +366,7 @@ module Turbo::Broadcastable
236
366
  # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
237
367
  # partial: "clearances/other_partial", locals: { a: 1 }
238
368
  def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
239
- Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
369
+ Turbo::StreamsChannel.broadcast_prepend_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
240
370
  end
241
371
 
242
372
  # Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
@@ -244,24 +374,36 @@ module Turbo::Broadcastable
244
374
  broadcast_prepend_to self, target: target, **rendering
245
375
  end
246
376
 
377
+ # Broadcast a "page refresh" to the stream name identified by the passed <tt>streamables</tt>. Example:
378
+ #
379
+ # # Sends <turbo-stream action="refresh"></turbo-stream> to the stream named "identity:2:clearances"
380
+ # clearance.broadcast_refresh_to examiner.identity, :clearances
381
+ def broadcast_refresh_to(*streamables)
382
+ Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
383
+ end
384
+
385
+ # Same as <tt>#broadcast_refresh_to</tt>, but the designated stream is automatically set to the current model.
386
+ def broadcast_refresh
387
+ broadcast_refresh_to self
388
+ end
389
+
247
390
  # Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
248
391
  #
249
392
  # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
250
393
  # # to the stream named "identity:2:clearances"
251
394
  # 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))
395
+ def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
396
+ Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
254
397
  end
255
398
 
256
399
  # 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
400
+ def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering)
401
+ broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering
259
402
  end
260
403
 
261
-
262
404
  # Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
263
405
  def broadcast_replace_later_to(*streamables, **rendering)
264
- Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
406
+ Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
265
407
  end
266
408
 
267
409
  # Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -271,7 +413,7 @@ module Turbo::Broadcastable
271
413
 
272
414
  # Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
273
415
  def broadcast_update_later_to(*streamables, **rendering)
274
- Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
416
+ Turbo::StreamsChannel.broadcast_update_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
275
417
  end
276
418
 
277
419
  # Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -281,7 +423,7 @@ module Turbo::Broadcastable
281
423
 
282
424
  # Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
283
425
  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))
426
+ Turbo::StreamsChannel.broadcast_append_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
285
427
  end
286
428
 
287
429
  # Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -291,7 +433,7 @@ module Turbo::Broadcastable
291
433
 
292
434
  # Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
293
435
  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))
436
+ Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
295
437
  end
296
438
 
297
439
  # Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -299,14 +441,24 @@ module Turbo::Broadcastable
299
441
  broadcast_prepend_later_to self, target: target, **rendering
300
442
  end
301
443
 
444
+ # Same as <tt>broadcast_refresh_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
445
+ def broadcast_refresh_later_to(*streamables)
446
+ Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
447
+ end
448
+
449
+ # Same as <tt>#broadcast_refresh_later_to</tt>, but the designated stream is automatically set to the current model.
450
+ def broadcast_refresh_later
451
+ broadcast_refresh_later_to self
452
+ end
453
+
302
454
  # 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))
455
+ def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
456
+ Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
305
457
  end
306
458
 
307
459
  # 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
460
+ def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering)
461
+ broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering
310
462
  end
311
463
 
312
464
  # Render a turbo stream template with this broadcastable model passed as the local variable. Example:
@@ -325,7 +477,7 @@ module Turbo::Broadcastable
325
477
  #
326
478
  # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
327
479
  # 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.
480
+ # be using +broadcast_render_later+, unless you specifically know why synchronous rendering is needed.
329
481
  def broadcast_render(**rendering)
330
482
  broadcast_render_to self, **rendering
331
483
  end
@@ -335,12 +487,12 @@ module Turbo::Broadcastable
335
487
  #
336
488
  # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
337
489
  # 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.
490
+ # be using +broadcast_render_later_to+, unless you specifically know why synchronous rendering is needed.
339
491
  def broadcast_render_to(*streamables, **rendering)
340
- Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
492
+ Turbo::StreamsChannel.broadcast_render_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
341
493
  end
342
494
 
343
- # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
495
+ # Same as <tt>broadcast_render_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
344
496
  def broadcast_render_later(**rendering)
345
497
  broadcast_render_later_to self, **rendering
346
498
  end
@@ -348,25 +500,32 @@ module Turbo::Broadcastable
348
500
  # Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
349
501
  # <tt>streamables</tt>.
350
502
  def broadcast_render_later_to(*streamables, **rendering)
351
- Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering))
503
+ Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **extract_options_and_add_target(rendering)) unless suppressed_turbo_broadcasts?
352
504
  end
353
505
 
354
-
355
506
  private
356
507
  def broadcast_target_default
357
508
  self.class.broadcast_target_default
358
509
  end
359
510
 
511
+ def extract_options_and_add_target(rendering = {}, target: broadcast_target_default)
512
+ broadcast_rendering_with_defaults(rendering).tap do |options|
513
+ options[:target] = target if !options.key?(:target) && !options.key?(:targets)
514
+ end
515
+ end
516
+
360
517
  def broadcast_rendering_with_defaults(options)
361
518
  options.tap do |o|
362
519
  # Add the current instance into the locals with the element name (which is the un-namespaced name)
363
520
  # 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)
521
+ o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self).compact
365
522
 
366
523
  if o[:html] || o[:partial]
367
524
  return o
368
525
  elsif o[:template] || o[:renderable]
369
526
  o[:layout] = false
527
+ elsif o[:render] == false
528
+ return o
370
529
  else
371
530
  # if none of these options are passed in, it will set a partial from #to_partial_path
372
531
  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
 
@@ -59,8 +77,9 @@ class Turbo::Streams::TagBuilder
59
77
  # <%= turbo_stream.replace "clearance_5" do %>
60
78
  # <div id='clearance_5'>Replace the dom target identified by clearance_5</div>
61
79
  # <% end %>
62
- def replace(target, content = nil, **rendering, &block)
63
- action :replace, target, content, **rendering, &block
80
+ # <%= turbo_stream.replace clearance, "<div>Morph the dom target</div>", method: :morph %>
81
+ def replace(target, content = nil, method: nil, **rendering, &block)
82
+ action :replace, target, content, method: method, **rendering, &block
64
83
  end
65
84
 
66
85
  # Replace the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
@@ -72,8 +91,9 @@ class Turbo::Streams::TagBuilder
72
91
  # <%= turbo_stream.replace_all ".clearance_item" do %>
73
92
  # <div class='.clearance_item'>Replace the dom target identified by the class clearance_item</div>
74
93
  # <% end %>
75
- def replace_all(targets, content = nil, **rendering, &block)
76
- action_all :replace, targets, content, **rendering, &block
94
+ # <%= turbo_stream.replace_all clearance, "<div>Morph the dom target</div>", method: :morph %>
95
+ def replace_all(targets, content = nil, method: nil, **rendering, &block)
96
+ action_all :replace, targets, content, method: method, **rendering, &block
77
97
  end
78
98
 
79
99
  # Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
@@ -137,8 +157,9 @@ class Turbo::Streams::TagBuilder
137
157
  # <%= turbo_stream.update "clearance_5" do %>
138
158
  # Update the content of the dom target identified by clearance_5
139
159
  # <% end %>
140
- def update(target, content = nil, **rendering, &block)
141
- action :update, target, content, **rendering, &block
160
+ # <%= turbo_stream.update clearance, "<div>Morph the dom target</div>", method: :morph %>
161
+ def update(target, content = nil, method: nil, **rendering, &block)
162
+ action :update, target, content, method: method, **rendering, &block
142
163
  end
143
164
 
144
165
  # Update the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
@@ -150,8 +171,9 @@ class Turbo::Streams::TagBuilder
150
171
  # <%= turbo_stream.update_all "clearance_item" do %>
151
172
  # Update the content of the dom target identified by the class clearance_item
152
173
  # <% end %>
153
- def update_all(targets, content = nil, **rendering, &block)
154
- action_all :update, targets, content, **rendering, &block
174
+ # <%= turbo_stream.update_all clearance, "<div>Morph the dom target</div>", method: :morph %>
175
+ def update_all(targets, content = nil, method: nil, **rendering, &block)
176
+ action_all :update, targets, content, method: method, **rendering, &block
155
177
  end
156
178
 
157
179
  # Append to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
@@ -210,23 +232,37 @@ class Turbo::Streams::TagBuilder
210
232
  action_all :prepend, targets, content, **rendering, &block
211
233
  end
212
234
 
235
+ # Creates a `turbo-stream` tag with an `[action="refresh"`] attribute and a
236
+ # `[request-id]` attribute that defaults to `Turbo.current_request_id`:
237
+ #
238
+ # turbo_stream.refresh
239
+ # # => <turbo-stream action="refresh" request-id="ef083d55-7516-41b1-ad28-16f553399c6a"></turbo-stream>
240
+ #
241
+ # turbo_stream.refresh request_id: "abc123"
242
+ # # => <turbo-stream action="refresh" request-id="abc123"></turbo-stream>
243
+ def refresh(...)
244
+ turbo_stream_refresh_tag(...)
245
+ end
246
+
213
247
  # Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
214
- def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
248
+ def action(name, target, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
215
249
  template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
216
250
 
217
- turbo_stream_action_tag name, target: target, template: template
251
+ turbo_stream_action_tag name, target: target, template: template, method: method
218
252
  end
219
253
 
220
254
  # Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
221
- def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
255
+ def action_all(name, targets, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
222
256
  template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
223
257
 
224
- turbo_stream_action_tag name, targets: targets, template: template
258
+ turbo_stream_action_tag name, targets: targets, template: template, method: method
225
259
  end
226
260
 
227
261
  private
228
262
  def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
229
263
  case
264
+ when target.respond_to?(:render_in) && content.nil?
265
+ target.render_in(@view_context, &block)
230
266
  when content.respond_to?(:render_in)
231
267
  content.render_in(@view_context, &block)
232
268
  when content
@@ -246,4 +282,6 @@ class Turbo::Streams::TagBuilder
246
282
  @view_context.render(partial: record, formats: :html)
247
283
  end
248
284
  end
285
+
286
+ ActiveSupport.run_load_hooks :turbo_streams_tag_builder, self
249
287
  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)