turbo-rails 0.5.2 → 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 +113 -18
- data/Rakefile +19 -2
- data/app/assets/javascripts/turbo.js +4143 -1431
- data/app/assets/javascripts/turbo.min.js +29 -0
- data/app/assets/javascripts/turbo.min.js.map +1 -0
- data/app/channels/turbo/streams/broadcasts.rb +58 -22
- data/app/channels/turbo/streams/stream_name.rb +7 -0
- data/app/channels/turbo/streams_channel.rb +30 -2
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/controllers/turbo/frames/frame_request.rb +22 -8
- data/app/controllers/turbo/native/navigation.rb +19 -9
- data/app/helpers/turbo/drive_helper.rb +75 -3
- data/app/helpers/turbo/frames_helper.rb +21 -2
- data/app/helpers/turbo/includes_helper.rb +2 -0
- data/app/helpers/turbo/streams/action_helper.rb +34 -9
- data/app/helpers/turbo/streams_helper.rb +20 -7
- data/app/javascript/turbo/cable.js +6 -3
- data/app/javascript/turbo/cable_stream_source_element.js +19 -3
- data/app/javascript/turbo/fetch_requests.js +59 -0
- data/app/javascript/turbo/index.js +6 -0
- data/app/javascript/turbo/snakeize.js +31 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +4 -2
- data/app/jobs/turbo/streams/broadcast_job.rb +2 -0
- data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +246 -38
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +163 -21
- data/app/models/turbo/thread_debouncer.rb +28 -0
- data/app/views/layouts/turbo_rails/frame.html.erb +8 -0
- data/config/routes.rb +1 -2
- data/lib/install/turbo_needs_redis.rb +20 -0
- data/lib/install/turbo_with_bun.rb +9 -0
- data/lib/install/turbo_with_importmap.rb +5 -0
- data/lib/install/turbo_with_node.rb +9 -0
- data/lib/tasks/turbo_tasks.rake +50 -8
- data/lib/turbo/broadcastable/test_helper.rb +172 -0
- data/lib/turbo/engine.rb +40 -6
- data/lib/turbo/test_assertions/integration_test_assertions.rb +76 -0
- data/lib/turbo/test_assertions.rb +69 -8
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +12 -0
- metadata +48 -173
- data/.github/workflows/ci.yml +0 -30
- data/.gitignore +0 -2
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -147
- data/lib/install/turbo_with_asset_pipeline.rb +0 -20
- data/lib/install/turbo_with_webpacker.rb +0 -24
- data/package.json +0 -47
- data/rollup.config.js +0 -23
- data/test/drive/drive_helper_test.rb +0 -8
- data/test/dummy/.babelrc +0 -18
- data/test/dummy/.gitignore +0 -3
- data/test/dummy/.postcssrc.yml +0 -3
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/config/manifest.js +0 -2
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/assets/stylesheets/scaffold.css +0 -80
- data/test/dummy/app/channels/application_cable/channel.rb +0 -4
- data/test/dummy/app/channels/application_cable/connection.rb +0 -4
- data/test/dummy/app/controllers/application_controller.rb +0 -2
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/messages_controller.rb +0 -12
- data/test/dummy/app/controllers/trays_controller.rb +0 -17
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/javascript/packs/application.js +0 -0
- data/test/dummy/app/jobs/application_job.rb +0 -2
- data/test/dummy/app/mailboxes/application_mailbox.rb +0 -2
- data/test/dummy/app/mailboxes/messages_mailbox.rb +0 -4
- data/test/dummy/app/mailers/application_mailer.rb +0 -4
- data/test/dummy/app/models/application_record.rb +0 -3
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/message.rb +0 -29
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
- data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
- data/test/dummy/app/views/messages/_message.html.erb +0 -1
- data/test/dummy/app/views/messages/_message.turbo_stream.erb +0 -1
- data/test/dummy/app/views/messages/show.turbo_stream.erb +0 -9
- data/test/dummy/app/views/trays/index.html.erb +0 -3
- data/test/dummy/app/views/trays/show.html.erb +0 -3
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -36
- data/test/dummy/bin/update +0 -31
- data/test/dummy/bin/yarn +0 -11
- data/test/dummy/config/application.rb +0 -22
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/cable.yml +0 -10
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -34
- data/test/dummy/config/environments/production.rb +0 -96
- data/test/dummy/config/environments/test.rb +0 -38
- data/test/dummy/config/initializers/application_controller_renderer.rb +0 -8
- data/test/dummy/config/initializers/assets.rb +0 -14
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/content_security_policy.rb +0 -22
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -5
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -33
- data/test/dummy/config/puma.rb +0 -34
- data/test/dummy/config/routes.rb +0 -4
- data/test/dummy/config/spring.rb +0 -6
- data/test/dummy/config/webpack/development.js +0 -3
- data/test/dummy/config/webpack/environment.js +0 -3
- data/test/dummy/config/webpack/production.js +0 -3
- data/test/dummy/config/webpack/test.js +0 -3
- data/test/dummy/config/webpacker.yml +0 -65
- data/test/dummy/config.ru +0 -5
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/frames/frame_request_controller_test.rb +0 -21
- data/test/frames/frames_helper_test.rb +0 -21
- data/test/native/navigation_controller_test.rb +0 -42
- data/test/streams/broadcastable_test.rb +0 -80
- data/test/streams/streams_channel_test.rb +0 -105
- data/test/streams/streams_controller_test.rb +0 -29
- data/test/turbo_test.rb +0 -10
- data/turbo-rails.gemspec +0 -17
- data/yarn.lock +0 -283
@@ -14,7 +14,7 @@
|
|
14
14
|
# <%= turbo_stream.append "entries" do %>
|
15
15
|
# <% # format is automatically switched, such that _entry.html.erb partial is rendered, not _entry.turbo_stream.erb %>
|
16
16
|
# <%= render partial: "entries/entry", locals: { entry: entry } %>
|
17
|
-
#
|
17
|
+
# <% end %>
|
18
18
|
#
|
19
19
|
# Or you can render the HTML that should be part of the update inline:
|
20
20
|
#
|
@@ -22,11 +22,30 @@
|
|
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
|
|
28
46
|
def initialize(view_context)
|
29
47
|
@view_context = view_context
|
48
|
+
@view_context.formats |= [:html]
|
30
49
|
end
|
31
50
|
|
32
51
|
# Removes the <tt>target</tt> from the dom. The target can either be a dom id string or an object that responds to
|
@@ -39,7 +58,17 @@ class Turbo::Streams::TagBuilder
|
|
39
58
|
action :remove, target, allow_inferred_rendering: false
|
40
59
|
end
|
41
60
|
|
42
|
-
#
|
61
|
+
# Removes the <tt>targets</tt> from the dom. The targets can either be a CSS selector string or an object that responds to
|
62
|
+
# <tt>to_key</tt>, which is then called and passed through <tt>ActionView::RecordIdentifier.dom_id</tt> (all Active Records
|
63
|
+
# do). Examples:
|
64
|
+
#
|
65
|
+
# <%= turbo_stream.remove_all ".clearance_item" %>
|
66
|
+
# <%= turbo_stream.remove_all clearance %>
|
67
|
+
def remove_all(targets)
|
68
|
+
action_all :remove, targets, allow_inferred_rendering: false
|
69
|
+
end
|
70
|
+
|
71
|
+
# Replace the <tt>target</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
|
43
72
|
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
|
44
73
|
#
|
45
74
|
# <%= turbo_stream.replace "clearance_5", "<div id='clearance_5'>Replace the dom target identified by clearance_5</div>" %>
|
@@ -52,7 +81,72 @@ class Turbo::Streams::TagBuilder
|
|
52
81
|
action :replace, target, content, **rendering, &block
|
53
82
|
end
|
54
83
|
|
55
|
-
#
|
84
|
+
# Replace the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
|
85
|
+
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
|
86
|
+
#
|
87
|
+
# <%= turbo_stream.replace_all ".clearance_item", "<div class='clearance_item'>Replace the dom target identified by the class clearance_item</div>" %>
|
88
|
+
# <%= turbo_stream.replace_all clearance %>
|
89
|
+
# <%= turbo_stream.replace_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
90
|
+
# <%= turbo_stream.replace_all ".clearance_item" do %>
|
91
|
+
# <div class='.clearance_item'>Replace the dom target identified by the class clearance_item</div>
|
92
|
+
# <% end %>
|
93
|
+
def replace_all(targets, content = nil, **rendering, &block)
|
94
|
+
action_all :replace, targets, content, **rendering, &block
|
95
|
+
end
|
96
|
+
|
97
|
+
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
98
|
+
# the content in the block, or the rendering of the target as a record before the <tt>target</tt> in the dom. Examples:
|
99
|
+
#
|
100
|
+
# <%= turbo_stream.before "clearance_5", "<div id='clearance_4'>Insert before the dom target identified by clearance_5</div>" %>
|
101
|
+
# <%= turbo_stream.before clearance %>
|
102
|
+
# <%= turbo_stream.before clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
103
|
+
# <%= turbo_stream.before "clearance_5" do %>
|
104
|
+
# <div id='clearance_4'>Insert before the dom target identified by clearance_5</div>
|
105
|
+
# <% end %>
|
106
|
+
def before(target, content = nil, **rendering, &block)
|
107
|
+
action :before, target, content, **rendering, &block
|
108
|
+
end
|
109
|
+
|
110
|
+
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
111
|
+
# the content in the block, or the rendering of the target as a record before the <tt>targets</tt> in the dom. Examples:
|
112
|
+
#
|
113
|
+
# <%= turbo_stream.before_all ".clearance_item", "<div class='clearance_item'>Insert before the dom target identified by the class clearance_item</div>" %>
|
114
|
+
# <%= turbo_stream.before_all clearance %>
|
115
|
+
# <%= turbo_stream.before_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
116
|
+
# <%= turbo_stream.before_all ".clearance_item" do %>
|
117
|
+
# <div class='clearance_item'>Insert before the dom target identified by clearance_item</div>
|
118
|
+
# <% end %>
|
119
|
+
def before_all(targets, content = nil, **rendering, &block)
|
120
|
+
action_all :before, targets, content, **rendering, &block
|
121
|
+
end
|
122
|
+
|
123
|
+
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
124
|
+
# the content in the block, or the rendering of the target as a record after the <tt>target</tt> in the dom. Examples:
|
125
|
+
#
|
126
|
+
# <%= turbo_stream.after "clearance_5", "<div id='clearance_6'>Insert after the dom target identified by clearance_5</div>" %>
|
127
|
+
# <%= turbo_stream.after clearance %>
|
128
|
+
# <%= turbo_stream.after clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
129
|
+
# <%= turbo_stream.after "clearance_5" do %>
|
130
|
+
# <div id='clearance_6'>Insert after the dom target identified by clearance_5</div>
|
131
|
+
# <% end %>
|
132
|
+
def after(target, content = nil, **rendering, &block)
|
133
|
+
action :after, target, content, **rendering, &block
|
134
|
+
end
|
135
|
+
|
136
|
+
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
137
|
+
# the content in the block, or the rendering of the target as a record after the <tt>targets</tt> in the dom. Examples:
|
138
|
+
#
|
139
|
+
# <%= turbo_stream.after_all ".clearance_item", "<div class='clearance_item'>Insert after the dom target identified by the class clearance_item</div>" %>
|
140
|
+
# <%= turbo_stream.after_all clearance %>
|
141
|
+
# <%= turbo_stream.after_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
142
|
+
# <%= turbo_stream.after_all "clearance_item" do %>
|
143
|
+
# <div class='clearance_item'>Insert after the dom target identified by the class clearance_item</div>
|
144
|
+
# <% end %>
|
145
|
+
def after_all(targets, content = nil, **rendering, &block)
|
146
|
+
action_all :after, targets, content, **rendering, &block
|
147
|
+
end
|
148
|
+
|
149
|
+
# Update the <tt>target</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
|
56
150
|
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
|
57
151
|
#
|
58
152
|
# <%= turbo_stream.update "clearance_5", "Update the content of the dom target identified by clearance_5" %>
|
@@ -65,6 +159,19 @@ class Turbo::Streams::TagBuilder
|
|
65
159
|
action :update, target, content, **rendering, &block
|
66
160
|
end
|
67
161
|
|
162
|
+
# Update the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
|
163
|
+
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the targets as a record. Examples:
|
164
|
+
#
|
165
|
+
# <%= turbo_stream.update_all "clearance_item", "Update the content of the dom target identified by the class clearance_item" %>
|
166
|
+
# <%= turbo_stream.update_all clearance %>
|
167
|
+
# <%= turbo_stream.update_all clearance, partial: "clearances/new_clearance", locals: { title: "Hello" } %>
|
168
|
+
# <%= turbo_stream.update_all "clearance_item" do %>
|
169
|
+
# Update the content of the dom target identified by the class clearance_item
|
170
|
+
# <% end %>
|
171
|
+
def update_all(targets, content = nil, **rendering, &block)
|
172
|
+
action_all :update, targets, content, **rendering, &block
|
173
|
+
end
|
174
|
+
|
68
175
|
# Append to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
|
69
176
|
# rendering result determined by the <tt>rendering</tt> keyword arguments, the content in the block,
|
70
177
|
# or the rendering of the content as a record. Examples:
|
@@ -79,6 +186,20 @@ class Turbo::Streams::TagBuilder
|
|
79
186
|
action :append, target, content, **rendering, &block
|
80
187
|
end
|
81
188
|
|
189
|
+
# Append to the targets in the dom identified with <tt>targets</tt> either the <tt>content</tt> passed in or a
|
190
|
+
# rendering result determined by the <tt>rendering</tt> keyword arguments, the content in the block,
|
191
|
+
# or the rendering of the content as a record. Examples:
|
192
|
+
#
|
193
|
+
# <%= turbo_stream.append_all ".clearances", "<div class='clearance_item'>Append this to .clearance_group</div>" %>
|
194
|
+
# <%= turbo_stream.append_all ".clearances", clearance %>
|
195
|
+
# <%= turbo_stream.append_all ".clearances", partial: "clearances/new_clearance", locals: { clearance: clearance } %>
|
196
|
+
# <%= turbo_stream.append_all ".clearances" do %>
|
197
|
+
# <div id='clearance_item'>Append this to .clearances</div>
|
198
|
+
# <% end %>
|
199
|
+
def append_all(targets, content = nil, **rendering, &block)
|
200
|
+
action_all :append, targets, content, **rendering, &block
|
201
|
+
end
|
202
|
+
|
82
203
|
# Prepend to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
|
83
204
|
# rendering result determined by the <tt>rendering</tt> keyword arguments or the content in the block,
|
84
205
|
# or the rendering of the content as a record. Examples:
|
@@ -93,35 +214,56 @@ class Turbo::Streams::TagBuilder
|
|
93
214
|
action :prepend, target, content, **rendering, &block
|
94
215
|
end
|
95
216
|
|
96
|
-
#
|
217
|
+
# Prepend to the targets in the dom identified with <tt>targets</tt> either the <tt>content</tt> passed in or a
|
218
|
+
# rendering result determined by the <tt>rendering</tt> keyword arguments or the content in the block,
|
219
|
+
# or the rendering of the content as a record. Examples:
|
220
|
+
#
|
221
|
+
# <%= turbo_stream.prepend_all ".clearances", "<div class='clearance_item'>Prepend this to .clearances</div>" %>
|
222
|
+
# <%= turbo_stream.prepend_all ".clearances", clearance %>
|
223
|
+
# <%= turbo_stream.prepend_all ".clearances", partial: "clearances/new_clearance", locals: { clearance: clearance } %>
|
224
|
+
# <%= turbo_stream.prepend_all ".clearances" do %>
|
225
|
+
# <div class='clearance_item'>Prepend this to .clearances</div>
|
226
|
+
# <% end %>
|
227
|
+
def prepend_all(targets, content = nil, **rendering, &block)
|
228
|
+
action_all :prepend, targets, content, **rendering, &block
|
229
|
+
end
|
230
|
+
|
231
|
+
# Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
|
97
232
|
def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
turbo_stream_action_tag name, target: target_name, template: (render_record(target) if allow_inferred_rendering)
|
109
|
-
end
|
233
|
+
template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
234
|
+
|
235
|
+
turbo_stream_action_tag name, target: target, template: template
|
236
|
+
end
|
237
|
+
|
238
|
+
# Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
|
239
|
+
def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
240
|
+
template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
241
|
+
|
242
|
+
turbo_stream_action_tag name, targets: targets, template: template
|
110
243
|
end
|
111
244
|
|
112
245
|
private
|
113
|
-
def
|
114
|
-
|
115
|
-
|
246
|
+
def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
247
|
+
case
|
248
|
+
when content.respond_to?(:render_in)
|
249
|
+
content.render_in(@view_context, &block)
|
250
|
+
when content
|
251
|
+
allow_inferred_rendering ? (render_record(content) || content) : content
|
252
|
+
when block_given?
|
253
|
+
@view_context.capture(&block)
|
254
|
+
when rendering.any?
|
255
|
+
@view_context.render(formats: [ :html ], **rendering)
|
116
256
|
else
|
117
|
-
target
|
257
|
+
render_record(target) if allow_inferred_rendering
|
118
258
|
end
|
119
259
|
end
|
120
260
|
|
121
261
|
def render_record(possible_record)
|
122
262
|
if possible_record.respond_to?(:to_partial_path)
|
123
263
|
record = possible_record
|
124
|
-
@view_context.render(partial: record
|
264
|
+
@view_context.render(partial: record, formats: :html)
|
125
265
|
end
|
126
266
|
end
|
267
|
+
|
268
|
+
ActiveSupport.run_load_hooks :turbo_streams_tag_builder, self
|
127
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
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
|
5
4
|
get "refresh_historical_location" => "turbo/native/navigation#refresh", as: :turbo_refresh_historical_location
|
6
|
-
end
|
5
|
+
end if Turbo.draw_routes
|
@@ -0,0 +1,20 @@
|
|
1
|
+
if (cable_config_path = Rails.root.join("config/cable.yml")).exist?
|
2
|
+
say "Enable redis in bundle"
|
3
|
+
|
4
|
+
gemfile_content = File.read(Rails.root.join("Gemfile"))
|
5
|
+
pattern = /gem ['"]redis['"]/
|
6
|
+
|
7
|
+
if gemfile_content.match?(pattern)
|
8
|
+
uncomment_lines "Gemfile", pattern
|
9
|
+
else
|
10
|
+
append_file "Gemfile", "\n# Use Redis for Action Cable"
|
11
|
+
gem 'redis', '~> 4.0'
|
12
|
+
end
|
13
|
+
|
14
|
+
run_bundle
|
15
|
+
|
16
|
+
say "Switch development cable to use redis"
|
17
|
+
gsub_file cable_config_path.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
|
18
|
+
else
|
19
|
+
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
|
20
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
if (js_entrypoint_path = Rails.root.join("app/javascript/application.js")).exist?
|
2
|
+
say "Import Turbo"
|
3
|
+
append_to_file "app/javascript/application.js", %(import "@hotwired/turbo-rails"\n)
|
4
|
+
else
|
5
|
+
say "You must import @hotwired/turbo-rails in your JavaScript entrypoint file", :red
|
6
|
+
end
|
7
|
+
|
8
|
+
say "Install Turbo"
|
9
|
+
run "bun add @hotwired/turbo-rails"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
if (js_entrypoint_path = Rails.root.join("app/javascript/application.js")).exist?
|
2
|
+
say "Import Turbo"
|
3
|
+
append_to_file "app/javascript/application.js", %(import "@hotwired/turbo-rails"\n)
|
4
|
+
else
|
5
|
+
say "You must import @hotwired/turbo-rails in your JavaScript entrypoint file", :red
|
6
|
+
end
|
7
|
+
|
8
|
+
say "Install Turbo"
|
9
|
+
run "yarn add @hotwired/turbo-rails"
|
data/lib/tasks/turbo_tasks.rake
CHANGED
@@ -1,24 +1,66 @@
|
|
1
|
-
|
1
|
+
module Turbo
|
2
|
+
module Tasks
|
3
|
+
extend self
|
4
|
+
def run_turbo_install_template(path)
|
5
|
+
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
|
6
|
+
end
|
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
|
+
def using_bun?
|
23
|
+
Rails.root.join("bun.config.js").exist?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
2
27
|
|
3
28
|
namespace :turbo do
|
4
29
|
desc "Install Turbo into the app"
|
5
30
|
task :install do
|
6
|
-
if
|
7
|
-
Rake::Task["turbo:install:
|
31
|
+
if Rails.root.join("config/importmap.rb").exist?
|
32
|
+
Rake::Task["turbo:install:importmap"].invoke
|
33
|
+
elsif Rails.root.join("package.json").exist? && Turbo::Tasks.using_bun?
|
34
|
+
Rake::Task["turbo:install:bun"].invoke
|
35
|
+
elsif Rails.root.join("package.json").exist?
|
36
|
+
Rake::Task["turbo:install:node"].invoke
|
8
37
|
else
|
9
|
-
|
38
|
+
puts "You must either be running with node (package.json) or importmap-rails (config/importmap.rb) to use this gem."
|
10
39
|
end
|
11
40
|
end
|
12
41
|
|
13
42
|
namespace :install do
|
14
43
|
desc "Install Turbo into the app with asset pipeline"
|
15
|
-
task :
|
16
|
-
|
44
|
+
task :importmap do
|
45
|
+
Turbo::Tasks.run_turbo_install_template "turbo_with_importmap"
|
46
|
+
Turbo::Tasks.switch_on_redis_if_available
|
17
47
|
end
|
18
48
|
|
19
49
|
desc "Install Turbo into the app with webpacker"
|
20
|
-
task :
|
21
|
-
|
50
|
+
task :node do
|
51
|
+
Turbo::Tasks.run_turbo_install_template "turbo_with_node"
|
52
|
+
Turbo::Tasks.switch_on_redis_if_available
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Install Turbo into the app with bun"
|
56
|
+
task :bun do
|
57
|
+
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"
|
22
64
|
end
|
23
65
|
end
|
24
66
|
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Turbo
|
2
|
+
module Broadcastable
|
3
|
+
module TestHelper
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActionCable::TestHelper
|
8
|
+
|
9
|
+
include Turbo::Streams::StreamName
|
10
|
+
end
|
11
|
+
|
12
|
+
# Asserts that `<turbo-stream>` elements were broadcast over Action Cable
|
13
|
+
#
|
14
|
+
# === Arguments
|
15
|
+
#
|
16
|
+
# * <tt>stream_name_or_object</tt> the objects used to generate the
|
17
|
+
# channel Action Cable name, or the name itself
|
18
|
+
# * <tt>&block</tt> optional block executed before the
|
19
|
+
# assertion
|
20
|
+
#
|
21
|
+
# === Options
|
22
|
+
#
|
23
|
+
# * <tt>count:</tt> the number of `<turbo-stream>` elements that are
|
24
|
+
# expected to be broadcast
|
25
|
+
#
|
26
|
+
# Asserts `<turbo-stream>` elements were broadcast:
|
27
|
+
#
|
28
|
+
# message = Message.find(1)
|
29
|
+
# message.broadcast_replace_to "messages"
|
30
|
+
#
|
31
|
+
# assert_turbo_stream_broadcasts "messages"
|
32
|
+
#
|
33
|
+
# Asserts that two `<turbo-stream>` elements were broadcast:
|
34
|
+
#
|
35
|
+
# message = Message.find(1)
|
36
|
+
# message.broadcast_replace_to "messages"
|
37
|
+
# message.broadcast_remove_to "messages"
|
38
|
+
#
|
39
|
+
# assert_turbo_stream_broadcasts "messages", count: 2
|
40
|
+
#
|
41
|
+
# You can pass a block to run before the assertion:
|
42
|
+
#
|
43
|
+
# message = Message.find(1)
|
44
|
+
#
|
45
|
+
# assert_turbo_stream_broadcasts "messages" do
|
46
|
+
# message.broadcast_append_to "messages"
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# In addition to a String, the helper also accepts an Object or Array to
|
50
|
+
# determine the name of the channel the elements are broadcast to:
|
51
|
+
#
|
52
|
+
# message = Message.find(1)
|
53
|
+
#
|
54
|
+
# assert_turbo_stream_broadcasts message do
|
55
|
+
# message.broadcast_replace
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
def assert_turbo_stream_broadcasts(stream_name_or_object, count: nil, &block)
|
59
|
+
payloads = capture_turbo_stream_broadcasts(stream_name_or_object, &block)
|
60
|
+
stream_name = stream_name_from(stream_name_or_object)
|
61
|
+
|
62
|
+
if count.nil?
|
63
|
+
assert_not_empty payloads, "Expected at least one broadcast on #{stream_name.inspect}, but there were none"
|
64
|
+
else
|
65
|
+
broadcasts = "Turbo Stream broadcast".pluralize(count)
|
66
|
+
|
67
|
+
assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were #{payloads.count}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Asserts that no `<turbo-stream>` elements were broadcast over Action Cable
|
72
|
+
#
|
73
|
+
# === Arguments
|
74
|
+
#
|
75
|
+
# * <tt>stream_name_or_object</tt> the objects used to generate the
|
76
|
+
# channel Action Cable name, or the name itself
|
77
|
+
# * <tt>&block</tt> optional block executed before the
|
78
|
+
# assertion
|
79
|
+
#
|
80
|
+
# Asserts that no `<turbo-stream>` elements were broadcast:
|
81
|
+
#
|
82
|
+
# message = Message.find(1)
|
83
|
+
# message.broadcast_replace_to "messages"
|
84
|
+
#
|
85
|
+
# assert_no_turbo_stream_broadcasts "messages" # fails with MiniTest::Assertion error
|
86
|
+
#
|
87
|
+
# You can pass a block to run before the assertion:
|
88
|
+
#
|
89
|
+
# message = Message.find(1)
|
90
|
+
#
|
91
|
+
# assert_no_turbo_stream_broadcasts "messages" do
|
92
|
+
# # do something other than broadcast to "messages"
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# In addition to a String, the helper also accepts an Object or Array to
|
96
|
+
# determine the name of the channel the elements are broadcast to:
|
97
|
+
#
|
98
|
+
# message = Message.find(1)
|
99
|
+
#
|
100
|
+
# assert_no_turbo_stream_broadcasts message do
|
101
|
+
# # do something other than broadcast to "message_1"
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
def assert_no_turbo_stream_broadcasts(stream_name_or_object, &block)
|
105
|
+
block&.call
|
106
|
+
|
107
|
+
stream_name = stream_name_from(stream_name_or_object)
|
108
|
+
|
109
|
+
payloads = broadcasts(stream_name)
|
110
|
+
|
111
|
+
assert payloads.empty?, "Expected no broadcasts on #{stream_name.inspect}, but there were #{payloads.count}"
|
112
|
+
end
|
113
|
+
|
114
|
+
# Captures any `<turbo-stream>` elements that were broadcast over Action Cable
|
115
|
+
#
|
116
|
+
# === Arguments
|
117
|
+
#
|
118
|
+
# * <tt>stream_name_or_object</tt> the objects used to generate the
|
119
|
+
# channel Action Cable name, or the name itself
|
120
|
+
# * <tt>&block</tt> optional block to capture broadcasts during execution
|
121
|
+
#
|
122
|
+
# Returns any `<turbo-stream>` elements that have been broadcast as an
|
123
|
+
# Array of <tt>Nokogiri::XML::Element</tt> instances
|
124
|
+
#
|
125
|
+
# message = Message.find(1)
|
126
|
+
# message.broadcast_append_to "messages"
|
127
|
+
# message.broadcast_prepend_to "messages"
|
128
|
+
#
|
129
|
+
# turbo_streams = capture_turbo_stream_broadcasts "messages"
|
130
|
+
#
|
131
|
+
# assert_equal "append", turbo_streams.first["action"]
|
132
|
+
# assert_equal "prepend", turbo_streams.second["action"]
|
133
|
+
#
|
134
|
+
# You can pass a block to limit the scope of the broadcasts being captured:
|
135
|
+
#
|
136
|
+
# message = Message.find(1)
|
137
|
+
#
|
138
|
+
# turbo_streams = capture_turbo_stream_broadcasts "messages" do
|
139
|
+
# message.broadcast_append_to "messages"
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# assert_equal "append", turbo_streams.first["action"]
|
143
|
+
#
|
144
|
+
# In addition to a String, the helper also accepts an Object or Array to
|
145
|
+
# determine the name of the channel the elements are broadcast to:
|
146
|
+
#
|
147
|
+
# message = Message.find(1)
|
148
|
+
#
|
149
|
+
# replace, remove = capture_turbo_stream_broadcasts message do
|
150
|
+
# message.broadcast_replace
|
151
|
+
# message.broadcast_remove
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# assert_equal "replace", replace["action"]
|
155
|
+
# assert_equal "replace", remove["action"]
|
156
|
+
#
|
157
|
+
def capture_turbo_stream_broadcasts(stream_name_or_object, &block)
|
158
|
+
block&.call
|
159
|
+
|
160
|
+
stream_name = stream_name_from(stream_name_or_object)
|
161
|
+
payloads = broadcasts(stream_name)
|
162
|
+
|
163
|
+
payloads.flat_map do |payload|
|
164
|
+
html = ActiveSupport::JSON.decode(payload)
|
165
|
+
document = Nokogiri::HTML5.parse(html)
|
166
|
+
|
167
|
+
document.at("body").element_children
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|