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.
- checksums.yaml +4 -4
- data/README.md +67 -16
- data/app/assets/javascripts/turbo.js +1974 -785
- 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 +33 -7
- data/app/channels/turbo/streams_channel.rb +15 -15
- 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 +12 -4
- data/app/helpers/turbo/streams_helper.rb +5 -0
- 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 +184 -35
- 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 +3 -4
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/broadcastable/test_helper.rb +5 -5
- data/lib/turbo/engine.rb +34 -8
- 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 +9 -5
- data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,4 +1,4 @@
|
|
1
|
-
# Turbo streams can be
|
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 [
|
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 +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
|
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
|
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>
|
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"
|
4
|
-
get "resume_historical_location"
|
5
|
-
get "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"
|
5
|
+
append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js"\n)
|
data/lib/tasks/turbo_tasks.rake
CHANGED
@@ -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
|
-
#
|
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
@@ -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
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
|