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.
- checksums.yaml +4 -4
- data/README.md +126 -16
- data/app/assets/javascripts/turbo.js +2226 -953
- 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 +47 -10
- 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 +17 -11
- 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/cable_stream_source_element.js +10 -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 +201 -42
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +50 -12
- 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 +80 -9
- data/lib/turbo/system_test_helper.rb +128 -0
- 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 +10 -19
- 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 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 [
|
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.
|
@@ -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
|
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
|
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
|
186
|
-
|
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
|
202
|
-
|
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
|
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
|
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,
|
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
|
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
|
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
|
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
|
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,
|
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
|
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
|
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, **
|
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>
|
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, **
|
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
|
-
|
63
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
141
|
-
|
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
|
-
|
154
|
-
|
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"
|
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)
|