turbo-rails 2.0.19 → 2.0.21
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/app/assets/javascripts/turbo.js +283 -286
- data/app/assets/javascripts/turbo.min.js +5 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +2 -2
- data/app/helpers/turbo/drive_helper.rb +2 -2
- data/app/helpers/turbo/frames_helper.rb +4 -1
- data/app/helpers/turbo/streams/action_helper.rb +4 -1
- data/app/models/concerns/turbo/broadcastable.rb +6 -6
- data/app/models/turbo/immediate_debouncer.rb +17 -0
- data/app/models/turbo/thread_debouncer.rb +3 -1
- data/lib/turbo/broadcastable/test_helper.rb +5 -3
- data/lib/turbo/engine.rb +5 -28
- data/lib/turbo/system_test_helper.rb +2 -0
- data/lib/turbo/test_assertions.rb +74 -0
- data/lib/turbo/version.rb +1 -1
- metadata +2 -1
|
@@ -33,8 +33,8 @@ module Turbo::Streams::Broadcasts
|
|
|
33
33
|
broadcast_action_to(*streamables, action: :prepend, **opts)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def broadcast_refresh_to(*streamables, **
|
|
37
|
-
broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag)
|
|
36
|
+
def broadcast_refresh_to(*streamables, **attributes)
|
|
37
|
+
broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag(**attributes))
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
|
|
@@ -74,13 +74,13 @@ module Turbo::DriveHelper
|
|
|
74
74
|
|
|
75
75
|
# Configure method to perform page refreshes. See +turbo_refreshes_with+.
|
|
76
76
|
def turbo_refresh_method_tag(method = :replace)
|
|
77
|
-
raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ])
|
|
77
|
+
raise ArgumentError, "Invalid refresh option '#{method}'" unless method.to_sym.in?(%i[ replace morph ])
|
|
78
78
|
tag.meta(name: "turbo-refresh-method", content: method)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Configure scroll strategy for page refreshes. See +turbo_refreshes_with+.
|
|
82
82
|
def turbo_refresh_scroll_tag(scroll = :reset)
|
|
83
|
-
raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ])
|
|
83
|
+
raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.to_sym.in?(%i[ reset preserve ])
|
|
84
84
|
tag.meta(name: "turbo-refresh-scroll", content: scroll)
|
|
85
85
|
end
|
|
86
86
|
end
|
|
@@ -33,10 +33,13 @@ module Turbo::FramesHelper
|
|
|
33
33
|
# <%= turbo_frame_tag(Article.find(1)) %>
|
|
34
34
|
# # => <turbo-frame id="article_1"></turbo-frame>
|
|
35
35
|
#
|
|
36
|
+
# <%= turbo_frame_tag(Article) %>
|
|
37
|
+
# # => <turbo-frame id="new_article"></turbo-frame>
|
|
38
|
+
#
|
|
36
39
|
# <%= turbo_frame_tag(Article.find(1), "comments") %>
|
|
37
40
|
# # => <turbo-frame id="comments_article_1"></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.join('_')
|
|
42
|
+
id = ids.first.respond_to?(:to_key) || ids.first.is_a?(Class) ? 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)
|
|
@@ -19,6 +19,9 @@ module Turbo::Streams::ActionHelper
|
|
|
19
19
|
# turbo_stream_action_tag "remove", target: message
|
|
20
20
|
# # => <turbo-stream action="remove" target="message_1"></turbo-stream>
|
|
21
21
|
#
|
|
22
|
+
# turbo_stream_action_tag "remove", target: Message
|
|
23
|
+
# # => <turbo-stream action="remove" target="new_message"></turbo-stream>
|
|
24
|
+
#
|
|
22
25
|
# message = Message.find(1)
|
|
23
26
|
# turbo_stream_action_tag "remove", target: [message, :special]
|
|
24
27
|
# # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
|
|
@@ -45,7 +48,7 @@ module Turbo::Streams::ActionHelper
|
|
|
45
48
|
private
|
|
46
49
|
def convert_to_turbo_stream_dom_id(target, include_selector: false)
|
|
47
50
|
target_array = Array.wrap(target)
|
|
48
|
-
if target_array.any? { |value| value.respond_to?(:to_key) }
|
|
51
|
+
if target_array.any? { |value| value.respond_to?(:to_key) || value.is_a?(Class) }
|
|
49
52
|
"#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(*target_array)}"
|
|
50
53
|
else
|
|
51
54
|
target
|
|
@@ -311,7 +311,7 @@ module Turbo::Broadcastable
|
|
|
311
311
|
def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
|
|
312
312
|
raise ArgumentError, "at least one of target or targets is required" unless target || targets
|
|
313
313
|
|
|
314
|
-
Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
|
|
314
|
+
Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets))) unless suppressed_turbo_broadcasts?
|
|
315
315
|
end
|
|
316
316
|
|
|
317
317
|
# Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
|
|
@@ -329,7 +329,7 @@ module Turbo::Broadcastable
|
|
|
329
329
|
def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
|
|
330
330
|
raise ArgumentError, "at least one of target or targets is required" unless target || targets
|
|
331
331
|
|
|
332
|
-
Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
|
|
332
|
+
Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets))) unless suppressed_turbo_broadcasts?
|
|
333
333
|
end
|
|
334
334
|
|
|
335
335
|
# Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
|
|
@@ -378,8 +378,8 @@ module Turbo::Broadcastable
|
|
|
378
378
|
#
|
|
379
379
|
# # Sends <turbo-stream action="refresh"></turbo-stream> to the stream named "identity:2:clearances"
|
|
380
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?
|
|
381
|
+
def broadcast_refresh_to(*streamables, **attributes)
|
|
382
|
+
Turbo::StreamsChannel.broadcast_refresh_to(*streamables, **attributes) unless suppressed_turbo_broadcasts?
|
|
383
383
|
end
|
|
384
384
|
|
|
385
385
|
# Same as <tt>#broadcast_refresh_to</tt>, but the designated stream is automatically set to the current model.
|
|
@@ -442,8 +442,8 @@ module Turbo::Broadcastable
|
|
|
442
442
|
end
|
|
443
443
|
|
|
444
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?
|
|
445
|
+
def broadcast_refresh_later_to(*streamables, **attributes)
|
|
446
|
+
Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **attributes) unless suppressed_turbo_broadcasts?
|
|
447
447
|
end
|
|
448
448
|
|
|
449
449
|
# Same as <tt>#broadcast_refresh_later_to</tt>, but the designated stream is automatically set to the current model.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# A debouncer that executes immediately without delays or background threads.
|
|
2
|
+
# This doesn't debounce at all, but is safe to use in tests.
|
|
3
|
+
class Turbo::ImmediateDebouncer # :nodoc:
|
|
4
|
+
def initialize(delay: Turbo::Debouncer::DEFAULT_DELAY)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def debounce(&block)
|
|
8
|
+
block.call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def wait
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def complete?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
class Turbo::ThreadDebouncer
|
|
4
4
|
delegate :wait, to: :debouncer
|
|
5
5
|
|
|
6
|
+
class_attribute :debouncer_class, default: Turbo::Debouncer
|
|
7
|
+
|
|
6
8
|
def self.for(key, delay: Turbo::Debouncer::DEFAULT_DELAY)
|
|
7
9
|
Thread.current[key] ||= new(key, Thread.current, delay: delay)
|
|
8
10
|
end
|
|
@@ -11,7 +13,7 @@ class Turbo::ThreadDebouncer
|
|
|
11
13
|
|
|
12
14
|
def initialize(key, thread, delay: )
|
|
13
15
|
@key = key
|
|
14
|
-
@debouncer =
|
|
16
|
+
@debouncer = debouncer_class.new(delay: delay)
|
|
15
17
|
@thread = thread
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -155,10 +155,12 @@ module Turbo
|
|
|
155
155
|
# assert_equal "replace", remove["action"]
|
|
156
156
|
#
|
|
157
157
|
def capture_turbo_stream_broadcasts(stream_name_or_object, &block)
|
|
158
|
-
block&.call
|
|
159
|
-
|
|
160
158
|
stream_name = stream_name_from(stream_name_or_object)
|
|
161
|
-
payloads =
|
|
159
|
+
payloads = if block_given?
|
|
160
|
+
new_broadcasts_from(broadcasts(stream_name), stream_name, "capture_turbo_stream_broadcasts", &block)
|
|
161
|
+
else
|
|
162
|
+
broadcasts(stream_name)
|
|
163
|
+
end
|
|
162
164
|
|
|
163
165
|
payloads.flat_map do |payload|
|
|
164
166
|
html = ActiveSupport::JSON.decode(payload)
|
data/lib/turbo/engine.rb
CHANGED
|
@@ -18,43 +18,17 @@ module Turbo
|
|
|
18
18
|
|
|
19
19
|
# If the parent application does not use Active Job, app/jobs cannot
|
|
20
20
|
# be eager loaded, because it references the ActiveJob constant.
|
|
21
|
-
#
|
|
22
|
-
# When turbo-rails depends on Rails 7 or above, the entire block can be
|
|
23
|
-
# reduced to
|
|
24
|
-
#
|
|
25
|
-
# unless defined?(ActiveJob)
|
|
26
|
-
# Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
|
|
27
|
-
# end
|
|
28
|
-
#
|
|
29
21
|
initializer "turbo.no_active_job", before: :set_eager_load_paths do
|
|
30
22
|
unless defined?(ActiveJob)
|
|
31
|
-
|
|
32
|
-
Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
|
|
33
|
-
else
|
|
34
|
-
# This else branch only runs in Rails 6.x + classic mode.
|
|
35
|
-
config.eager_load_paths.delete("#{root}/app/jobs")
|
|
36
|
-
end
|
|
23
|
+
Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
|
|
37
24
|
end
|
|
38
25
|
end
|
|
39
26
|
|
|
40
27
|
# If the parent application does not use Action Cable, app/channels cannot
|
|
41
28
|
# be eager loaded, because it references the ActionCable constant.
|
|
42
|
-
#
|
|
43
|
-
# When turbo-rails depends on Rails 7 or above, the entire block can be
|
|
44
|
-
# reduced to
|
|
45
|
-
#
|
|
46
|
-
# unless defined?(ActionCable)
|
|
47
|
-
# Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
|
|
48
|
-
# end
|
|
49
|
-
#
|
|
50
29
|
initializer "turbo.no_action_cable", before: :set_eager_load_paths do
|
|
51
30
|
unless defined?(ActionCable)
|
|
52
|
-
|
|
53
|
-
Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
|
|
54
|
-
else
|
|
55
|
-
# This else branch only runs in Rails 6.x + classic mode.
|
|
56
|
-
config.eager_load_paths.delete("#{root}/app/channels")
|
|
57
|
-
end
|
|
31
|
+
Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
|
|
58
32
|
end
|
|
59
33
|
end
|
|
60
34
|
|
|
@@ -121,6 +95,9 @@ module Turbo
|
|
|
121
95
|
ActiveSupport.on_load(:active_support_test_case) do
|
|
122
96
|
require "turbo/test_assertions"
|
|
123
97
|
include Turbo::TestAssertions
|
|
98
|
+
|
|
99
|
+
# Use ImmediateDebouncer in tests to prevent flaky tests from background threads
|
|
100
|
+
Turbo::ThreadDebouncer.debouncer_class = Turbo::ImmediateDebouncer
|
|
124
101
|
end
|
|
125
102
|
|
|
126
103
|
ActiveSupport.on_load(:action_cable) do
|
|
@@ -80,6 +80,8 @@ module Turbo::SystemTestHelper
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
Capybara.add_selector :turbo_cable_stream_source do
|
|
83
|
+
visible :all
|
|
84
|
+
|
|
83
85
|
xpath do |locator|
|
|
84
86
|
xpath = XPath.descendant.where(XPath.local_name == "turbo-cable-stream-source")
|
|
85
87
|
xpath.where(SignedStreamNameConditions.new(locator).reduce(:|))
|
|
@@ -79,5 +79,79 @@ module Turbo
|
|
|
79
79
|
selector << %([targets="#{targets}"]) if targets
|
|
80
80
|
assert_select selector, count: 0
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
# Assert that the rendered fragment of HTML contains a `<turbo-frame>`
|
|
84
|
+
# element.
|
|
85
|
+
#
|
|
86
|
+
# ==== Arguments
|
|
87
|
+
#
|
|
88
|
+
# * <tt>ids</tt> [String, Array<String, ActiveRecord::Base>] matches the element's <tt>[id]</tt> attribute
|
|
89
|
+
#
|
|
90
|
+
# ==== Options
|
|
91
|
+
#
|
|
92
|
+
# * <tt>:loading</tt> [String] matches the element's <tt>[loading]</tt>
|
|
93
|
+
# attribute
|
|
94
|
+
# * <tt>:src</tt> [String] matches the element's <tt>[src]</tt> attribute
|
|
95
|
+
# * <tt>:target</tt> [String] matches the element's <tt>[target]</tt>
|
|
96
|
+
# attribute
|
|
97
|
+
# * <tt>:count</tt> [Integer] indicates how many turbo frames are expected.
|
|
98
|
+
# Defaults to <tt>1</tt>.
|
|
99
|
+
#
|
|
100
|
+
# Given the following HTML fragment:
|
|
101
|
+
#
|
|
102
|
+
# <turbo-frame id="example" target="_top"></turbo-frame>
|
|
103
|
+
#
|
|
104
|
+
# The following assertion would pass:
|
|
105
|
+
#
|
|
106
|
+
# assert_turbo_frame id: "example", target: "_top"
|
|
107
|
+
#
|
|
108
|
+
# You can also pass a block make assertions about the contents of the
|
|
109
|
+
# element. Given the following HTML fragment:
|
|
110
|
+
#
|
|
111
|
+
# <turbo-frame id="example">
|
|
112
|
+
# <p>Hello!</p>
|
|
113
|
+
# </turbo-frame>
|
|
114
|
+
#
|
|
115
|
+
# The following assertion would pass:
|
|
116
|
+
#
|
|
117
|
+
# assert_turbo_frame id: "example" do
|
|
118
|
+
# assert_select "p", text: "Hello!"
|
|
119
|
+
# end
|
|
120
|
+
#
|
|
121
|
+
def assert_turbo_frame(*ids, loading: nil, src: nil, target: nil, count: 1, &block)
|
|
122
|
+
id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
|
|
123
|
+
selector = %(turbo-frame[id="#{id}"])
|
|
124
|
+
selector << %([loading="#{loading}"]) if loading
|
|
125
|
+
selector << %([src="#{src}"]) if src
|
|
126
|
+
selector << %([target="#{target}"]) if target
|
|
127
|
+
assert_select selector, count: count, &block
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Assert that the rendered fragment of HTML does not contain a `<turbo-frame>`
|
|
131
|
+
# element.
|
|
132
|
+
#
|
|
133
|
+
# ==== Arguments
|
|
134
|
+
#
|
|
135
|
+
# * <tt>ids</tt> [String, Array<String, ActiveRecord::Base>] matches the <tt>[id]</tt> attribute
|
|
136
|
+
#
|
|
137
|
+
# ==== Options
|
|
138
|
+
#
|
|
139
|
+
# * <tt>:loading</tt> [String] matches the element's <tt>[loading]</tt>
|
|
140
|
+
# attribute
|
|
141
|
+
# * <tt>:src</tt> [String] matches the element's <tt>[src]</tt> attribute
|
|
142
|
+
# * <tt>:target</tt> [String] matches the element's <tt>[target]</tt>
|
|
143
|
+
# attribute
|
|
144
|
+
#
|
|
145
|
+
# Given the following HTML fragment:
|
|
146
|
+
#
|
|
147
|
+
# <turbo-frame id="example" target="_top"></turbo-frame>
|
|
148
|
+
#
|
|
149
|
+
# The following assertion would fail:
|
|
150
|
+
#
|
|
151
|
+
# assert_no_turbo_frame id: "example", target: "_top"
|
|
152
|
+
#
|
|
153
|
+
def assert_no_turbo_frame(*ids, **options, &block)
|
|
154
|
+
assert_turbo_frame(*ids, **options, count: 0, &block)
|
|
155
|
+
end
|
|
82
156
|
end
|
|
83
157
|
end
|
data/lib/turbo/version.rb
CHANGED
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: 2.0.
|
|
4
|
+
version: 2.0.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Stephenson
|
|
@@ -73,6 +73,7 @@ files:
|
|
|
73
73
|
- app/jobs/turbo/streams/broadcast_stream_job.rb
|
|
74
74
|
- app/models/concerns/turbo/broadcastable.rb
|
|
75
75
|
- app/models/turbo/debouncer.rb
|
|
76
|
+
- app/models/turbo/immediate_debouncer.rb
|
|
76
77
|
- app/models/turbo/streams/tag_builder.rb
|
|
77
78
|
- app/models/turbo/thread_debouncer.rb
|
|
78
79
|
- app/views/layouts/turbo_rails/frame.html.erb
|