turbo-rails 1.5.0 → 2.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -11
- data/app/assets/javascripts/turbo.js +1896 -726
- data/app/assets/javascripts/turbo.min.js +9 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +19 -5
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/controllers/turbo/frames/frame_request.rb +2 -2
- data/app/controllers/turbo/native/navigation.rb +6 -3
- data/app/helpers/turbo/drive_helper.rb +72 -14
- data/app/helpers/turbo/frames_helper.rb +8 -8
- data/app/helpers/turbo/streams/action_helper.rb +9 -2
- data/app/helpers/turbo/streams_helper.rb +0 -1
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +175 -34
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +20 -0
- data/app/models/turbo/thread_debouncer.rb +28 -0
- data/config/routes.rb +0 -1
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/turbo/broadcastable/test_helper.rb +5 -5
- data/lib/turbo/engine.rb +13 -2
- data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
- data/lib/turbo/test_assertions.rb +2 -2
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +6 -2
@@ -14,8 +14,8 @@
|
|
14
14
|
# end
|
15
15
|
# end
|
16
16
|
#
|
17
|
-
# This is an example from [
|
18
|
-
#
|
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
|
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
|
44
|
-
# Again, only to any broadcast method that accepts the
|
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
|
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
|
71
|
-
# <tt>prepend</tt
|
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
|
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 +broadcast_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
|
+
# broadcast_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 +broadcast_refreshes_to+ method:
|
108
|
+
#
|
109
|
+
# class Column < ApplicationRecord
|
110
|
+
# belongs_to :board
|
111
|
+
# broadcast_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.
|
@@ -143,7 +261,7 @@ module Turbo::Broadcastable
|
|
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 }
|
145
263
|
def broadcast_replace_to(*streamables, **rendering)
|
146
|
-
Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
264
|
+
Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
147
265
|
end
|
148
266
|
|
149
267
|
# Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -162,7 +280,7 @@ module Turbo::Broadcastable
|
|
162
280
|
# # to the stream named "identity:2:clearances"
|
163
281
|
# clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
164
282
|
def broadcast_update_to(*streamables, **rendering)
|
165
|
-
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
283
|
+
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
166
284
|
end
|
167
285
|
|
168
286
|
# Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -215,7 +333,7 @@ module Turbo::Broadcastable
|
|
215
333
|
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
|
216
334
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
217
335
|
def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
|
218
|
-
Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
|
336
|
+
Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
219
337
|
end
|
220
338
|
|
221
339
|
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -236,7 +354,7 @@ module Turbo::Broadcastable
|
|
236
354
|
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
|
237
355
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
238
356
|
def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
|
239
|
-
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
|
357
|
+
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
240
358
|
end
|
241
359
|
|
242
360
|
# Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -244,24 +362,36 @@ module Turbo::Broadcastable
|
|
244
362
|
broadcast_prepend_to self, target: target, **rendering
|
245
363
|
end
|
246
364
|
|
365
|
+
# Broadcast a "page refresh" to the stream name identified by the passed <tt>streamables</tt>. Example:
|
366
|
+
#
|
367
|
+
# # Sends <turbo-stream action="refresh"></turbo-stream> to the stream named "identity:2:clearances"
|
368
|
+
# clearance.broadcast_refresh_to examiner.identity, :clearances
|
369
|
+
def broadcast_refresh_to(*streamables)
|
370
|
+
Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
|
371
|
+
end
|
372
|
+
|
373
|
+
# Same as <tt>#broadcast_refresh_to</tt>, but the designated stream is automatically set to the current model.
|
374
|
+
def broadcast_refresh
|
375
|
+
broadcast_refresh_to self
|
376
|
+
end
|
377
|
+
|
247
378
|
# Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
|
248
379
|
#
|
249
380
|
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
250
381
|
# # to the stream named "identity:2:clearances"
|
251
382
|
# 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))
|
383
|
+
def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
|
384
|
+
Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
254
385
|
end
|
255
386
|
|
256
387
|
# 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
|
388
|
+
def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering)
|
389
|
+
broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering
|
259
390
|
end
|
260
391
|
|
261
|
-
|
262
392
|
# Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
263
393
|
def broadcast_replace_later_to(*streamables, **rendering)
|
264
|
-
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
394
|
+
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
265
395
|
end
|
266
396
|
|
267
397
|
# Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -271,7 +401,7 @@ module Turbo::Broadcastable
|
|
271
401
|
|
272
402
|
# Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
273
403
|
def broadcast_update_later_to(*streamables, **rendering)
|
274
|
-
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
404
|
+
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
275
405
|
end
|
276
406
|
|
277
407
|
# Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -281,7 +411,7 @@ module Turbo::Broadcastable
|
|
281
411
|
|
282
412
|
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
283
413
|
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))
|
414
|
+
Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
285
415
|
end
|
286
416
|
|
287
417
|
# Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -291,7 +421,7 @@ module Turbo::Broadcastable
|
|
291
421
|
|
292
422
|
# Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
293
423
|
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))
|
424
|
+
Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
295
425
|
end
|
296
426
|
|
297
427
|
# Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -299,14 +429,24 @@ module Turbo::Broadcastable
|
|
299
429
|
broadcast_prepend_later_to self, target: target, **rendering
|
300
430
|
end
|
301
431
|
|
432
|
+
# Same as <tt>broadcast_refresh_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
433
|
+
def broadcast_refresh_later_to(*streamables)
|
434
|
+
Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
|
435
|
+
end
|
436
|
+
|
437
|
+
# Same as <tt>#broadcast_refresh_later_to</tt>, but the designated stream is automatically set to the current model.
|
438
|
+
def broadcast_refresh_later
|
439
|
+
broadcast_refresh_later_to self
|
440
|
+
end
|
441
|
+
|
302
442
|
# 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))
|
443
|
+
def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
|
444
|
+
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
305
445
|
end
|
306
446
|
|
307
447
|
# 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
|
448
|
+
def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering)
|
449
|
+
broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering
|
310
450
|
end
|
311
451
|
|
312
452
|
# Render a turbo stream template with this broadcastable model passed as the local variable. Example:
|
@@ -325,7 +465,7 @@ module Turbo::Broadcastable
|
|
325
465
|
#
|
326
466
|
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
|
327
467
|
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
|
328
|
-
# be using
|
468
|
+
# be using +broadcast_render_later+, unless you specifically know why synchronous rendering is needed.
|
329
469
|
def broadcast_render(**rendering)
|
330
470
|
broadcast_render_to self, **rendering
|
331
471
|
end
|
@@ -335,12 +475,12 @@ module Turbo::Broadcastable
|
|
335
475
|
#
|
336
476
|
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
|
337
477
|
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
|
338
|
-
# be using
|
478
|
+
# be using +broadcast_render_later_to+, unless you specifically know why synchronous rendering is needed.
|
339
479
|
def broadcast_render_to(*streamables, **rendering)
|
340
|
-
Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
|
480
|
+
Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
341
481
|
end
|
342
482
|
|
343
|
-
# Same as <tt>
|
483
|
+
# Same as <tt>broadcast_render_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
344
484
|
def broadcast_render_later(**rendering)
|
345
485
|
broadcast_render_later_to self, **rendering
|
346
486
|
end
|
@@ -348,10 +488,9 @@ module Turbo::Broadcastable
|
|
348
488
|
# Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
|
349
489
|
# <tt>streamables</tt>.
|
350
490
|
def broadcast_render_later_to(*streamables, **rendering)
|
351
|
-
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering))
|
491
|
+
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
352
492
|
end
|
353
493
|
|
354
|
-
|
355
494
|
private
|
356
495
|
def broadcast_target_default
|
357
496
|
self.class.broadcast_target_default
|
@@ -361,12 +500,14 @@ module Turbo::Broadcastable
|
|
361
500
|
options.tap do |o|
|
362
501
|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
363
502
|
# 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)
|
503
|
+
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
|
365
504
|
|
366
505
|
if o[:html] || o[:partial]
|
367
506
|
return o
|
368
507
|
elsif o[:template] || o[:renderable]
|
369
508
|
o[:layout] = false
|
509
|
+
elsif o[:render] == false
|
510
|
+
return o
|
370
511
|
else
|
371
512
|
# if none of these options are passed in, it will set a partial from #to_partial_path
|
372
513
|
o[:partial] ||= to_partial_path
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Turbo::Debouncer
|
2
|
+
attr_reader :delay, :scheduled_task
|
3
|
+
|
4
|
+
DEFAULT_DELAY = 0.5
|
5
|
+
|
6
|
+
def initialize(delay: DEFAULT_DELAY)
|
7
|
+
@delay = delay
|
8
|
+
@scheduled_task = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def debounce(&block)
|
12
|
+
scheduled_task&.cancel unless scheduled_task&.complete?
|
13
|
+
@scheduled_task = Concurrent::ScheduledTask.execute(delay, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def wait
|
17
|
+
scheduled_task&.wait(wait_timeout)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def wait_timeout
|
22
|
+
delay + 1
|
23
|
+
end
|
24
|
+
end
|
@@ -22,6 +22,24 @@
|
|
22
22
|
# <%= turbo_stream.append dom_id(topic_merge) do %>
|
23
23
|
# <%= link_to topic_merge.topic.name, topic_path(topic_merge.topic) %>
|
24
24
|
# <% end %>
|
25
|
+
#
|
26
|
+
# To integrate with custom actions, extend this class in response to the :turbo_streams_tag_builder load hook:
|
27
|
+
#
|
28
|
+
# ActiveSupport.on_load :turbo_streams_tag_builder do
|
29
|
+
# def highlight(target)
|
30
|
+
# action :highlight, target
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def highlight_all(targets)
|
34
|
+
# action_all :highlight, targets
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# turbo_stream.highlight "my-element"
|
39
|
+
# # => <turbo-stream action="highlight" target="my-element"><template></template></turbo-stream>
|
40
|
+
#
|
41
|
+
# turbo_stream.highlight_all ".my-selector"
|
42
|
+
# # => <turbo-stream action="highlight" targets=".my-selector"><template></template></turbo-stream>
|
25
43
|
class Turbo::Streams::TagBuilder
|
26
44
|
include Turbo::Streams::ActionHelper
|
27
45
|
|
@@ -246,4 +264,6 @@ class Turbo::Streams::TagBuilder
|
|
246
264
|
@view_context.render(partial: record, formats: :html)
|
247
265
|
end
|
248
266
|
end
|
267
|
+
|
268
|
+
ActiveSupport.run_load_hooks :turbo_streams_tag_builder, self
|
249
269
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# A decorated debouncer that will store instances in the current thread clearing them
|
2
|
+
# after the debounced logic triggers.
|
3
|
+
class Turbo::ThreadDebouncer
|
4
|
+
delegate :wait, to: :debouncer
|
5
|
+
|
6
|
+
def self.for(key, delay: Turbo::Debouncer::DEFAULT_DELAY)
|
7
|
+
Thread.current[key] ||= new(key, Thread.current, delay: delay)
|
8
|
+
end
|
9
|
+
|
10
|
+
private_class_method :new
|
11
|
+
|
12
|
+
def initialize(key, thread, delay: )
|
13
|
+
@key = key
|
14
|
+
@debouncer = Turbo::Debouncer.new(delay: delay)
|
15
|
+
@thread = thread
|
16
|
+
end
|
17
|
+
|
18
|
+
def debounce
|
19
|
+
debouncer.debounce do
|
20
|
+
yield.tap do
|
21
|
+
thread[key] = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
attr_reader :key, :debouncer, :thread
|
28
|
+
end
|
data/config/routes.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# FIXME: Offer flag to opt out of these native routes
|
2
1
|
Rails.application.routes.draw do
|
3
2
|
get "recede_historical_location" => "turbo/native/navigation#recede", as: :turbo_recede_historical_location
|
4
3
|
get "resume_historical_location" => "turbo/native/navigation#resume", as: :turbo_resume_historical_location
|
@@ -2,4 +2,4 @@ say "Import Turbo"
|
|
2
2
|
append_to_file "app/javascript/application.js", %(import "@hotwired/turbo-rails"\n)
|
3
3
|
|
4
4
|
say "Pin Turbo"
|
5
|
-
append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
5
|
+
append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js"\n)
|
@@ -11,14 +11,14 @@ module Turbo
|
|
11
11
|
|
12
12
|
# Asserts that `<turbo-stream>` elements were broadcast over Action Cable
|
13
13
|
#
|
14
|
-
#
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
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
@@ -46,6 +46,12 @@ module Turbo
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
initializer "turbo.request_id_tracking" do
|
50
|
+
ActiveSupport.on_load(:action_controller) do
|
51
|
+
include Turbo::RequestIdTracking
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
49
55
|
initializer "turbo.broadcastable" do
|
50
56
|
ActiveSupport.on_load(:active_record) do
|
51
57
|
include Turbo::Broadcastable
|
@@ -75,11 +81,16 @@ module Turbo
|
|
75
81
|
initializer "turbo.test_assertions" do
|
76
82
|
ActiveSupport.on_load(:active_support_test_case) do
|
77
83
|
require "turbo/test_assertions"
|
78
|
-
require "turbo/broadcastable/test_helper"
|
79
|
-
|
80
84
|
include Turbo::TestAssertions
|
81
85
|
end
|
82
86
|
|
87
|
+
ActiveSupport.on_load(:action_cable) do
|
88
|
+
ActiveSupport.on_load(:active_support_test_case) do
|
89
|
+
require "turbo/broadcastable/test_helper"
|
90
|
+
include Turbo::Broadcastable::TestHelper
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
83
94
|
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
84
95
|
require "turbo/test_assertions/integration_test_assertions"
|
85
96
|
|
@@ -4,7 +4,7 @@ module Turbo
|
|
4
4
|
# Assert that the Turbo Stream request's response body's HTML contains a
|
5
5
|
# `<turbo-stream>` element.
|
6
6
|
#
|
7
|
-
#
|
7
|
+
# ==== Options
|
8
8
|
#
|
9
9
|
# * <tt>:status</tt> [Integer, Symbol] the HTTP response status
|
10
10
|
# * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
|
@@ -47,7 +47,7 @@ module Turbo
|
|
47
47
|
# Assert that the Turbo Stream request's response body's HTML does not
|
48
48
|
# contain a `<turbo-stream>` element.
|
49
49
|
#
|
50
|
-
#
|
50
|
+
# ==== Options
|
51
51
|
#
|
52
52
|
# * <tt>:status</tt> [Integer, Symbol] the HTTP response status
|
53
53
|
# * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
|
@@ -10,7 +10,7 @@ module Turbo
|
|
10
10
|
# Assert that the rendered fragment of HTML contains a `<turbo-stream>`
|
11
11
|
# element.
|
12
12
|
#
|
13
|
-
#
|
13
|
+
# ==== Options
|
14
14
|
#
|
15
15
|
# * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
|
16
16
|
# attribute
|
@@ -55,7 +55,7 @@ module Turbo
|
|
55
55
|
# Assert that the rendered fragment of HTML does not contain a `<turbo-stream>`
|
56
56
|
# element.
|
57
57
|
#
|
58
|
-
#
|
58
|
+
# ==== Options
|
59
59
|
#
|
60
60
|
# * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
|
61
61
|
# attribute
|
data/lib/turbo/version.rb
CHANGED
data/lib/turbo-rails.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require "turbo/engine"
|
2
|
+
require "active_support/core_ext/module/attribute_accessors_per_thread"
|
2
3
|
|
3
4
|
module Turbo
|
4
5
|
extend ActiveSupport::Autoload
|
5
6
|
|
6
7
|
mattr_accessor :draw_routes, default: true
|
7
8
|
|
9
|
+
thread_mattr_accessor :current_request_id
|
10
|
+
|
8
11
|
class << self
|
9
12
|
attr_writer :signed_stream_verifier_key
|
10
13
|
|
@@ -15,5 +18,12 @@ module Turbo
|
|
15
18
|
def signed_stream_verifier_key
|
16
19
|
@signed_stream_verifier_key or raise ArgumentError, "Turbo requires a signed_stream_verifier_key"
|
17
20
|
end
|
21
|
+
|
22
|
+
def with_request_id(request_id)
|
23
|
+
old_request_id, self.current_request_id = self.current_request_id, request_id
|
24
|
+
yield
|
25
|
+
ensure
|
26
|
+
self.current_request_id = old_request_id
|
27
|
+
end
|
18
28
|
end
|
19
29
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Stephenson
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2024-02-21 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activejob
|
@@ -69,6 +69,7 @@ files:
|
|
69
69
|
- app/channels/turbo/streams/broadcasts.rb
|
70
70
|
- app/channels/turbo/streams/stream_name.rb
|
71
71
|
- app/channels/turbo/streams_channel.rb
|
72
|
+
- app/controllers/concerns/turbo/request_id_tracking.rb
|
72
73
|
- app/controllers/turbo/frames/frame_request.rb
|
73
74
|
- app/controllers/turbo/native/navigation.rb
|
74
75
|
- app/controllers/turbo/native/navigation_controller.rb
|
@@ -85,8 +86,11 @@ files:
|
|
85
86
|
- app/javascript/turbo/snakeize.js
|
86
87
|
- app/jobs/turbo/streams/action_broadcast_job.rb
|
87
88
|
- app/jobs/turbo/streams/broadcast_job.rb
|
89
|
+
- app/jobs/turbo/streams/broadcast_stream_job.rb
|
88
90
|
- app/models/concerns/turbo/broadcastable.rb
|
91
|
+
- app/models/turbo/debouncer.rb
|
89
92
|
- app/models/turbo/streams/tag_builder.rb
|
93
|
+
- app/models/turbo/thread_debouncer.rb
|
90
94
|
- app/views/layouts/turbo_rails/frame.html.erb
|
91
95
|
- config/routes.rb
|
92
96
|
- lib/install/turbo_needs_redis.rb
|