turbo-rails 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -6
- data/app/assets/javascripts/turbo.js +1894 -725
- 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 +18 -4
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/helpers/turbo/drive_helper.rb +63 -4
- data/app/helpers/turbo/frames_helper.rb +4 -1
- data/app/helpers/turbo/streams/action_helper.rb +5 -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 +87 -20
- 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 +1 -1
- data/lib/turbo/engine.rb +6 -0
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +6 -2
@@ -33,9 +33,14 @@ module Turbo::Streams::Broadcasts
|
|
33
33
|
broadcast_action_to(*streamables, action: :prepend, **opts)
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
36
|
+
def broadcast_refresh_to(*streamables, **opts)
|
37
|
+
broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag)
|
38
|
+
end
|
39
|
+
|
40
|
+
def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
|
37
41
|
broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
|
38
|
-
rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil)
|
42
|
+
rendering.delete(:content) || rendering.delete(:html) || (rendering[:render] != false && rendering.any? ? render_format(:html, **rendering) : nil),
|
43
|
+
**attributes
|
39
44
|
))
|
40
45
|
end
|
41
46
|
|
@@ -63,9 +68,15 @@ module Turbo::Streams::Broadcasts
|
|
63
68
|
broadcast_action_later_to(*streamables, action: :prepend, **opts)
|
64
69
|
end
|
65
70
|
|
66
|
-
def
|
71
|
+
def broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **opts)
|
72
|
+
refresh_debouncer_for(*streamables, request_id: request_id).debounce do
|
73
|
+
Turbo::Streams::BroadcastStreamJob.perform_later stream_name_from(streamables), content: turbo_stream_refresh_tag(request_id: request_id, **opts)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
|
67
78
|
Turbo::Streams::ActionBroadcastJob.perform_later \
|
68
|
-
stream_name_from(streamables), action: action, target: target, targets: targets, **rendering
|
79
|
+
stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
|
69
80
|
end
|
70
81
|
|
71
82
|
def broadcast_render_to(*streamables, **rendering)
|
@@ -80,6 +91,9 @@ module Turbo::Streams::Broadcasts
|
|
80
91
|
ActionCable.server.broadcast stream_name_from(streamables), content
|
81
92
|
end
|
82
93
|
|
94
|
+
def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
|
95
|
+
Turbo::ThreadDebouncer.for("turbo-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
|
96
|
+
end
|
83
97
|
|
84
98
|
private
|
85
99
|
def render_format(format, **rendering)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Turbo::RequestIdTracking
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
around_action :turbo_tracking_request_id
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def turbo_tracking_request_id(&block)
|
10
|
+
Turbo.with_request_id(request.headers["X-Turbo-Request-Id"], &block)
|
11
|
+
end
|
12
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
module Turbo::DriveHelper
|
2
|
-
#
|
2
|
+
# Helpers to configure Turbo Drive via meta directives. They come in two
|
3
|
+
# variants:
|
4
|
+
#
|
5
|
+
# The recommended option is to include +yield :head+ in the +<head>+ section
|
6
|
+
# of the layout. Then you can use the helpers in any view.
|
3
7
|
#
|
4
8
|
# ==== Example
|
5
9
|
#
|
@@ -9,21 +13,76 @@ module Turbo::DriveHelper
|
|
9
13
|
# # app/views/trays/index.html.erb
|
10
14
|
# <% turbo_exempts_page_from_cache %>
|
11
15
|
# <p>Page that shouldn't be cached by Turbo</p>
|
16
|
+
#
|
17
|
+
# Alternatively, you can use the +_tag+ variant of the helpers to only get the
|
18
|
+
# HTML for the meta directive.
|
12
19
|
|
13
20
|
# Pages that are more likely than not to be a cache miss can skip turbo cache to avoid visual jitter.
|
14
21
|
# Cannot be used along with +turbo_exempts_page_from_preview+.
|
15
22
|
def turbo_exempts_page_from_cache
|
16
|
-
provide :head,
|
23
|
+
provide :head, turbo_exempts_page_from_cache_tag
|
24
|
+
end
|
25
|
+
|
26
|
+
# See +turbo_exempts_page_from_cache+.
|
27
|
+
def turbo_exempts_page_from_cache_tag
|
28
|
+
tag.meta(name: "turbo-cache-control", content: "no-cache")
|
17
29
|
end
|
18
30
|
|
19
31
|
# Specify that a cached version of the page should not be shown as a preview during an application visit.
|
20
32
|
# Cannot be used along with +turbo_exempts_page_from_cache+.
|
21
33
|
def turbo_exempts_page_from_preview
|
22
|
-
provide :head,
|
34
|
+
provide :head, turbo_exempts_page_from_preview_tag
|
35
|
+
end
|
36
|
+
|
37
|
+
# See +turbo_exempts_page_from_preview+.
|
38
|
+
def turbo_exempts_page_from_preview_tag
|
39
|
+
tag.meta(name: "turbo-cache-control", content: "no-preview")
|
23
40
|
end
|
24
41
|
|
25
42
|
# Force the page, when loaded by Turbo, to be cause a full page reload.
|
26
43
|
def turbo_page_requires_reload
|
27
|
-
provide :head,
|
44
|
+
provide :head, turbo_page_requires_reload_tag
|
45
|
+
end
|
46
|
+
|
47
|
+
# See +turbo_page_requires_reload+.
|
48
|
+
def turbo_page_requires_reload_tag
|
49
|
+
tag.meta(name: "turbo-visit-control", content: "reload")
|
50
|
+
end
|
51
|
+
|
52
|
+
# Configure how to handle page refreshes. A page refresh happens when
|
53
|
+
# Turbo loads the current page again with a *replace* visit:
|
54
|
+
#
|
55
|
+
# === Parameters:
|
56
|
+
#
|
57
|
+
# * <tt>method</tt> - Method to update the +<body>+ of the page
|
58
|
+
# during a page refresh. It can be one of:
|
59
|
+
# * +replace:+: Replaces the existing +<body>+ with the new one. This is the
|
60
|
+
# default behavior.
|
61
|
+
# * +morph:+: Morphs the existing +<body>+ into the new one.
|
62
|
+
#
|
63
|
+
# * <tt>scroll</tt> - Controls the scroll behavior when a page refresh happens. It
|
64
|
+
# can be one of:
|
65
|
+
# * +reset:+: Resets scroll to the top, left corner. This is the default.
|
66
|
+
# * +preserve:+: Keeps the scroll.
|
67
|
+
#
|
68
|
+
# === Example Usage:
|
69
|
+
#
|
70
|
+
# turbo_refreshes_with(method: :morph, scroll: :preserve)
|
71
|
+
def turbo_refreshes_with(method: :replace, scroll: :reset)
|
72
|
+
provide :head, turbo_refresh_method_tag(method)
|
73
|
+
provide :head, turbo_refresh_scroll_tag(scroll)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Configure method to perform page refreshes. See +turbo_refreshes_with+.
|
77
|
+
def turbo_refresh_method_tag(method = :replace)
|
78
|
+
raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ])
|
79
|
+
tag.meta(name: "turbo-refresh-method", content: method)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Configure scroll strategy for page refreshes. See +turbo_refreshes_with+.
|
83
|
+
def turbo_refresh_scroll_tag(scroll = :reset)
|
84
|
+
raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ])
|
85
|
+
tag.meta(name: "turbo-refresh-scroll", content: scroll)
|
28
86
|
end
|
29
87
|
end
|
88
|
+
|
@@ -24,6 +24,9 @@ module Turbo::FramesHelper
|
|
24
24
|
# <% end %>
|
25
25
|
# # => <turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
|
26
26
|
#
|
27
|
+
# <%= turbo_frame_tag [user_id, "tray"], src: tray_path(tray) %>
|
28
|
+
# # => <turbo-frame id="1_tray" src="http://example.com/trays/1"></turbo-frame>
|
29
|
+
#
|
27
30
|
# The `turbo_frame_tag` helper will convert the arguments it receives to their
|
28
31
|
# `dom_id` if applicable to easily generate unique ids for Turbo Frames:
|
29
32
|
#
|
@@ -36,7 +39,7 @@ module Turbo::FramesHelper
|
|
36
39
|
# <%= turbo_frame_tag(Article.find(1), Comment.new) %>
|
37
40
|
# # => <turbo-frame id="article_1_new_comment"></turbo-frame>
|
38
41
|
def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
|
39
|
-
id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.
|
42
|
+
id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
|
40
43
|
src = url_for(src) if src.present?
|
41
44
|
|
42
45
|
tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
|
@@ -24,7 +24,7 @@ module Turbo::Streams::ActionHelper
|
|
24
24
|
# # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
|
25
25
|
#
|
26
26
|
def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, **attributes)
|
27
|
-
template = action.to_sym
|
27
|
+
template = action.to_sym.in?(%i[ remove refresh ]) ? "" : tag.template(template.to_s.html_safe)
|
28
28
|
|
29
29
|
if target = convert_to_turbo_stream_dom_id(target)
|
30
30
|
tag.turbo_stream(template, **attributes, action: action, target: target)
|
@@ -35,6 +35,10 @@ module Turbo::Streams::ActionHelper
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
+
def turbo_stream_refresh_tag(request_id: Turbo.current_request_id, **attributes)
|
39
|
+
turbo_stream_action_tag(:refresh, **{ "request-id": request_id }.compact, **attributes)
|
40
|
+
end
|
41
|
+
|
38
42
|
private
|
39
43
|
def convert_to_turbo_stream_dom_id(target, include_selector: false)
|
40
44
|
if Array(target).any? { |value| value.respond_to?(:to_key) }
|
@@ -2,7 +2,7 @@
|
|
2
2
|
class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base
|
3
3
|
discard_on ActiveJob::DeserializationError
|
4
4
|
|
5
|
-
def perform(stream, action:, target:, **rendering)
|
6
|
-
Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering
|
5
|
+
def perform(stream, action:, target:, attributes: {}, **rendering)
|
6
|
+
Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, attributes: attributes, **rendering
|
7
7
|
end
|
8
8
|
end
|
@@ -75,9 +75,32 @@
|
|
75
75
|
# In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
|
76
76
|
# <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
|
77
77
|
# to render a turbo stream template with multiple actions.
|
78
|
+
#
|
79
|
+
# == Suppressing broadcasts
|
80
|
+
#
|
81
|
+
# Sometimes, you need to disable broadcasts in certain scenarios. You can use <tt>.suppressing_turbo_broadcasts</tt> to create
|
82
|
+
# execution contexts where broadcasts are disabled:
|
83
|
+
#
|
84
|
+
# class Message < ApplicationRecord
|
85
|
+
# after_create_commit :update_message
|
86
|
+
#
|
87
|
+
# private
|
88
|
+
# def update_message
|
89
|
+
# broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# Message.suppressing_turbo_broadcasts do
|
94
|
+
# Message.create!(board: board) # This won't broadcast the replace action
|
95
|
+
# end
|
78
96
|
module Turbo::Broadcastable
|
79
97
|
extend ActiveSupport::Concern
|
80
98
|
|
99
|
+
included do
|
100
|
+
thread_mattr_accessor :suppressed_turbo_broadcasts, instance_accessor: false
|
101
|
+
delegate :suppressed_turbo_broadcasts?, to: "self.class"
|
102
|
+
end
|
103
|
+
|
81
104
|
module ClassMethods
|
82
105
|
# Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
|
83
106
|
# <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
|
@@ -112,10 +135,36 @@ module Turbo::Broadcastable
|
|
112
135
|
after_destroy_commit -> { broadcast_remove }
|
113
136
|
end
|
114
137
|
|
138
|
+
# Configures the model to broadcast a "page refresh" on creates, updates, and destroys to a stream
|
139
|
+
# name derived at runtime by the <tt>stream</tt> symbol invocation.
|
140
|
+
def broadcasts_refreshes_to(stream)
|
141
|
+
after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Same as <tt>#broadcasts_refreshes_to</tt>, but the designated stream for page refreshes is automatically set to
|
145
|
+
# the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
|
146
|
+
def broadcasts_refreshes(stream = model_name.plural)
|
147
|
+
after_create_commit -> { broadcast_refresh_later_to(stream) }
|
148
|
+
after_update_commit -> { broadcast_refresh_later }
|
149
|
+
after_destroy_commit -> { broadcast_refresh }
|
150
|
+
end
|
151
|
+
|
115
152
|
# All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
|
116
153
|
def broadcast_target_default
|
117
154
|
model_name.plural
|
118
155
|
end
|
156
|
+
|
157
|
+
# Executes +block+ preventing both synchronous and asynchronous broadcasts from this model.
|
158
|
+
def suppressing_turbo_broadcasts(&block)
|
159
|
+
original, self.suppressed_turbo_broadcasts = self.suppressed_turbo_broadcasts, true
|
160
|
+
yield
|
161
|
+
ensure
|
162
|
+
self.suppressed_turbo_broadcasts = original
|
163
|
+
end
|
164
|
+
|
165
|
+
def suppressed_turbo_broadcasts?
|
166
|
+
suppressed_turbo_broadcasts
|
167
|
+
end
|
119
168
|
end
|
120
169
|
|
121
170
|
# Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
|
@@ -124,7 +173,7 @@ module Turbo::Broadcastable
|
|
124
173
|
# # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
|
125
174
|
# clearance.broadcast_remove_to examiner.identity, :clearances
|
126
175
|
def broadcast_remove_to(*streamables, target: self)
|
127
|
-
Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target)
|
176
|
+
Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
|
128
177
|
end
|
129
178
|
|
130
179
|
# Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -143,7 +192,7 @@ module Turbo::Broadcastable
|
|
143
192
|
# # to the stream named "identity:2:clearances"
|
144
193
|
# clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
145
194
|
def broadcast_replace_to(*streamables, **rendering)
|
146
|
-
Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
195
|
+
Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
147
196
|
end
|
148
197
|
|
149
198
|
# Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -162,7 +211,7 @@ module Turbo::Broadcastable
|
|
162
211
|
# # to the stream named "identity:2:clearances"
|
163
212
|
# clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
164
213
|
def broadcast_update_to(*streamables, **rendering)
|
165
|
-
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
214
|
+
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
166
215
|
end
|
167
216
|
|
168
217
|
# Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -215,7 +264,7 @@ module Turbo::Broadcastable
|
|
215
264
|
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
|
216
265
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
217
266
|
def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
|
218
|
-
Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
|
267
|
+
Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
219
268
|
end
|
220
269
|
|
221
270
|
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -236,7 +285,7 @@ module Turbo::Broadcastable
|
|
236
285
|
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
|
237
286
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
238
287
|
def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
|
239
|
-
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
|
288
|
+
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
240
289
|
end
|
241
290
|
|
242
291
|
# Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -244,24 +293,32 @@ module Turbo::Broadcastable
|
|
244
293
|
broadcast_prepend_to self, target: target, **rendering
|
245
294
|
end
|
246
295
|
|
296
|
+
def broadcast_refresh_to(*streamables)
|
297
|
+
Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
|
298
|
+
end
|
299
|
+
|
300
|
+
def broadcast_refresh
|
301
|
+
broadcast_refresh_to self
|
302
|
+
end
|
303
|
+
|
247
304
|
# Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
|
248
305
|
#
|
249
306
|
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
250
307
|
# # to the stream named "identity:2:clearances"
|
251
308
|
# 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))
|
309
|
+
def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
|
310
|
+
Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
254
311
|
end
|
255
312
|
|
256
313
|
# 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
|
314
|
+
def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering)
|
315
|
+
broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering
|
259
316
|
end
|
260
317
|
|
261
318
|
|
262
319
|
# Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
263
320
|
def broadcast_replace_later_to(*streamables, **rendering)
|
264
|
-
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
321
|
+
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
265
322
|
end
|
266
323
|
|
267
324
|
# Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -271,7 +328,7 @@ module Turbo::Broadcastable
|
|
271
328
|
|
272
329
|
# Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
273
330
|
def broadcast_update_later_to(*streamables, **rendering)
|
274
|
-
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
|
331
|
+
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
275
332
|
end
|
276
333
|
|
277
334
|
# Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -281,7 +338,7 @@ module Turbo::Broadcastable
|
|
281
338
|
|
282
339
|
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
283
340
|
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))
|
341
|
+
Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
285
342
|
end
|
286
343
|
|
287
344
|
# Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -291,7 +348,7 @@ module Turbo::Broadcastable
|
|
291
348
|
|
292
349
|
# Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
293
350
|
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))
|
351
|
+
Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
295
352
|
end
|
296
353
|
|
297
354
|
# Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -299,14 +356,22 @@ module Turbo::Broadcastable
|
|
299
356
|
broadcast_prepend_later_to self, target: target, **rendering
|
300
357
|
end
|
301
358
|
|
359
|
+
def broadcast_refresh_later_to(*streamables)
|
360
|
+
Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
|
361
|
+
end
|
362
|
+
|
363
|
+
def broadcast_refresh_later
|
364
|
+
broadcast_refresh_later_to self
|
365
|
+
end
|
366
|
+
|
302
367
|
# 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))
|
368
|
+
def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
|
369
|
+
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
305
370
|
end
|
306
371
|
|
307
372
|
# 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
|
373
|
+
def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering)
|
374
|
+
broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering
|
310
375
|
end
|
311
376
|
|
312
377
|
# Render a turbo stream template with this broadcastable model passed as the local variable. Example:
|
@@ -337,7 +402,7 @@ module Turbo::Broadcastable
|
|
337
402
|
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
|
338
403
|
# be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
|
339
404
|
def broadcast_render_to(*streamables, **rendering)
|
340
|
-
Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
|
405
|
+
Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
341
406
|
end
|
342
407
|
|
343
408
|
# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
@@ -348,7 +413,7 @@ module Turbo::Broadcastable
|
|
348
413
|
# Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
|
349
414
|
# <tt>streamables</tt>.
|
350
415
|
def broadcast_render_later_to(*streamables, **rendering)
|
351
|
-
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering))
|
416
|
+
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
352
417
|
end
|
353
418
|
|
354
419
|
|
@@ -361,12 +426,14 @@ module Turbo::Broadcastable
|
|
361
426
|
options.tap do |o|
|
362
427
|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
363
428
|
# 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)
|
429
|
+
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
|
365
430
|
|
366
431
|
if o[:html] || o[:partial]
|
367
432
|
return o
|
368
433
|
elsif o[:template] || o[:renderable]
|
369
434
|
o[:layout] = false
|
435
|
+
elsif o[:render] == false
|
436
|
+
return o
|
370
437
|
else
|
371
438
|
# if none of these options are passed in, it will set a partial from #to_partial_path
|
372
439
|
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)
|
@@ -64,7 +64,7 @@ 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
|
|
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
|
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.0
|
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-07 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
|