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
@@ -0,0 +1,59 @@
|
|
1
|
+
export function encodeMethodIntoRequestBody(event) {
|
2
|
+
if (event.target instanceof HTMLFormElement) {
|
3
|
+
const { target: form, detail: { fetchOptions } } = event
|
4
|
+
|
5
|
+
form.addEventListener("turbo:submit-start", ({ detail: { formSubmission: { submitter } } }) => {
|
6
|
+
const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams()
|
7
|
+
const method = determineFetchMethod(submitter, body, form)
|
8
|
+
|
9
|
+
if (!/get/i.test(method)) {
|
10
|
+
if (/post/i.test(method)) {
|
11
|
+
body.delete("_method")
|
12
|
+
} else {
|
13
|
+
body.set("_method", method)
|
14
|
+
}
|
15
|
+
|
16
|
+
fetchOptions.method = "post"
|
17
|
+
}
|
18
|
+
}, { once: true })
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
function determineFetchMethod(submitter, body, form) {
|
23
|
+
const formMethod = determineFormMethod(submitter)
|
24
|
+
const overrideMethod = body.get("_method")
|
25
|
+
const method = form.getAttribute("method") || "get"
|
26
|
+
|
27
|
+
if (typeof formMethod == "string") {
|
28
|
+
return formMethod
|
29
|
+
} else if (typeof overrideMethod == "string") {
|
30
|
+
return overrideMethod
|
31
|
+
} else {
|
32
|
+
return method
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
function determineFormMethod(submitter) {
|
37
|
+
if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
|
38
|
+
// Rails 7 ActionView::Helpers::FormBuilder#button method has an override
|
39
|
+
// for formmethod if the button does not have name or value attributes
|
40
|
+
// set, which is the default. This means that if you use <%= f.button
|
41
|
+
// formmethod: :delete %>, it will generate a <button name="_method"
|
42
|
+
// value="delete" formmethod="post">. Therefore, if the submitter's name
|
43
|
+
// is already _method, it's value attribute already contains the desired
|
44
|
+
// method.
|
45
|
+
if (submitter.name === '_method') {
|
46
|
+
return submitter.value
|
47
|
+
} else if (submitter.hasAttribute("formmethod")) {
|
48
|
+
return submitter.formMethod
|
49
|
+
} else {
|
50
|
+
return null
|
51
|
+
}
|
52
|
+
} else {
|
53
|
+
return null
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
function isBodyInit(body) {
|
58
|
+
return body instanceof FormData || body instanceof URLSearchParams
|
59
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
// Based on https://github.com/nathan7/snakeize
|
2
|
+
//
|
3
|
+
// This software is released under the MIT license:
|
4
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
// this software and associated documentation files (the "Software"), to deal in
|
6
|
+
// the Software without restriction, including without limitation the rights to
|
7
|
+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
// the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
// subject to the following conditions:
|
10
|
+
|
11
|
+
// The above copyright notice and this permission notice shall be included in all
|
12
|
+
// copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
export default function walk (obj) {
|
21
|
+
if (!obj || typeof obj !== 'object') return obj;
|
22
|
+
if (obj instanceof Date || obj instanceof RegExp) return obj;
|
23
|
+
if (Array.isArray(obj)) return obj.map(walk);
|
24
|
+
return Object.keys(obj).reduce(function (acc, key) {
|
25
|
+
var camel = key[0].toLowerCase() + key.slice(1).replace(/([A-Z]+)/g, function (m, x) {
|
26
|
+
return '_' + x.toLowerCase();
|
27
|
+
});
|
28
|
+
acc[camel] = walk(obj[key]);
|
29
|
+
return acc;
|
30
|
+
}, {});
|
31
|
+
};
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# The job that powers all the <tt>broadcast_$action_later</tt> broadcasts available in <tt>Turbo::Streams::Broadcasts</tt>.
|
2
2
|
class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base
|
3
|
-
|
4
|
-
|
3
|
+
discard_on ActiveJob::DeserializationError
|
4
|
+
|
5
|
+
def perform(stream, action:, target:, attributes: {}, **rendering)
|
6
|
+
Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, attributes: attributes, **rendering
|
5
7
|
end
|
6
8
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# The job that powers the <tt>broadcast_render_later_to</tt> available in <tt>Turbo::Streams::Broadcasts</tt> for rendering
|
2
2
|
# turbo stream templates.
|
3
3
|
class Turbo::Streams::BroadcastJob < ActiveJob::Base
|
4
|
+
discard_on ActiveJob::DeserializationError
|
5
|
+
|
4
6
|
def perform(stream, **rendering)
|
5
7
|
Turbo::StreamsChannel.broadcast_render_to stream, **rendering
|
6
8
|
end
|
@@ -26,20 +26,85 @@
|
|
26
26
|
# and finally prepend the result of that partial rendering to the target identified with the dom id "clearances"
|
27
27
|
# (which is derived by default from the plural model name of the model, but can be overwritten).
|
28
28
|
#
|
29
|
+
# You can also choose to render html instead of a partial inside of a broadcast
|
30
|
+
# you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
|
31
|
+
#
|
32
|
+
# class Message < ApplicationRecord
|
33
|
+
# belongs_to :user
|
34
|
+
#
|
35
|
+
# after_create_commit :update_message_count
|
36
|
+
#
|
37
|
+
# private
|
38
|
+
# def update_message_count
|
39
|
+
# broadcast_update_to(user, :messages, target: "message-count", html: "<p> #{user.messages.count} </p>")
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
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
|
+
#
|
46
|
+
# class Message < ApplicationRecord
|
47
|
+
# belongs_to :user
|
48
|
+
#
|
49
|
+
# after_create_commit :update_message
|
50
|
+
#
|
51
|
+
# private
|
52
|
+
# def update_message
|
53
|
+
# broadcast_replace_to(user, :message, target: "message", template: "messages/show", locals: { message: self })
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# If you want to render a renderable object you can use the `renderable:` option.
|
58
|
+
#
|
59
|
+
# class Message < ApplicationRecord
|
60
|
+
# belongs_to :user
|
61
|
+
#
|
62
|
+
# after_create_commit :update_message
|
63
|
+
#
|
64
|
+
# private
|
65
|
+
# def update_message
|
66
|
+
# broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
29
70
|
# There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
|
30
71
|
# <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
|
31
72
|
# within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
|
32
73
|
# execution. You don't need to do this for remove, since only the dom id for the model is used.
|
33
74
|
#
|
34
|
-
# In addition to the four basic actions, you can also use <tt>
|
35
|
-
# <tt>
|
75
|
+
# In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
|
76
|
+
# <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
|
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
|
36
96
|
module Turbo::Broadcastable
|
37
97
|
extend ActiveSupport::Concern
|
38
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
|
+
|
39
104
|
module ClassMethods
|
40
105
|
# Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
|
41
106
|
# <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
|
42
|
-
# the model's plural name. The insertion can also be made to be a prepend by overwriting <tt>
|
107
|
+
# the model's plural name. The insertion can also be made to be a prepend by overwriting <tt>inserts_by</tt> and
|
43
108
|
# the target dom id overwritten by passing <tt>target</tt>. Examples:
|
44
109
|
#
|
45
110
|
# class Message < ApplicationRecord
|
@@ -51,18 +116,55 @@ module Turbo::Broadcastable
|
|
51
116
|
# belongs_to :board
|
52
117
|
# broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages"
|
53
118
|
# end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
119
|
+
#
|
120
|
+
# class Message < ApplicationRecord
|
121
|
+
# belongs_to :board
|
122
|
+
# broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
|
123
|
+
# end
|
124
|
+
def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default, **rendering)
|
125
|
+
after_create_commit -> { broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering) }
|
126
|
+
after_update_commit -> { broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
|
127
|
+
after_destroy_commit -> { broadcast_remove_to(stream.try(:call, self) || send(stream)) }
|
58
128
|
end
|
59
129
|
|
60
|
-
# Same as <tt>#broadcasts_to</tt>, but the designated stream is automatically set to
|
61
|
-
|
62
|
-
|
63
|
-
|
130
|
+
# Same as <tt>#broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
|
131
|
+
# the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
|
132
|
+
def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcast_target_default, **rendering)
|
133
|
+
after_create_commit -> { broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering) }
|
134
|
+
after_update_commit -> { broadcast_replace_later(**rendering) }
|
64
135
|
after_destroy_commit -> { broadcast_remove }
|
65
136
|
end
|
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
|
+
|
152
|
+
# All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
|
153
|
+
def broadcast_target_default
|
154
|
+
model_name.plural
|
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
|
66
168
|
end
|
67
169
|
|
68
170
|
# Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
|
@@ -70,8 +172,8 @@ module Turbo::Broadcastable
|
|
70
172
|
#
|
71
173
|
# # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
|
72
174
|
# clearance.broadcast_remove_to examiner.identity, :clearances
|
73
|
-
def broadcast_remove_to(*streamables)
|
74
|
-
Turbo::StreamsChannel.broadcast_remove_to
|
175
|
+
def broadcast_remove_to(*streamables, target: self)
|
176
|
+
Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
|
75
177
|
end
|
76
178
|
|
77
179
|
# Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -90,7 +192,7 @@ module Turbo::Broadcastable
|
|
90
192
|
# # to the stream named "identity:2:clearances"
|
91
193
|
# clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
92
194
|
def broadcast_replace_to(*streamables, **rendering)
|
93
|
-
Turbo::StreamsChannel.broadcast_replace_to
|
195
|
+
Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
94
196
|
end
|
95
197
|
|
96
198
|
# Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -98,6 +200,57 @@ module Turbo::Broadcastable
|
|
98
200
|
broadcast_replace_to self, **rendering
|
99
201
|
end
|
100
202
|
|
203
|
+
# Update this broadcastable model in the dom for subscribers of the stream name identified by the passed
|
204
|
+
# <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
|
205
|
+
#
|
206
|
+
# # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
207
|
+
# # to the stream named "identity:2:clearances"
|
208
|
+
# clearance.broadcast_update_to examiner.identity, :clearances
|
209
|
+
#
|
210
|
+
# # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
|
211
|
+
# # to the stream named "identity:2:clearances"
|
212
|
+
# clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
213
|
+
def broadcast_update_to(*streamables, **rendering)
|
214
|
+
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
215
|
+
end
|
216
|
+
|
217
|
+
# Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
|
218
|
+
def broadcast_update(**rendering)
|
219
|
+
broadcast_update_to self, **rendering
|
220
|
+
end
|
221
|
+
|
222
|
+
# Insert a rendering of this broadcastable model before the target identified by it's dom id passed as <tt>target</tt>
|
223
|
+
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
|
224
|
+
# appending named arguments to the call. Examples:
|
225
|
+
#
|
226
|
+
# # Sends <turbo-stream action="before" target="clearance_5"><template><div id="clearance_4">My Clearance</div></template></turbo-stream>
|
227
|
+
# # to the stream named "identity:2:clearances"
|
228
|
+
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5"
|
229
|
+
#
|
230
|
+
# # Sends <turbo-stream action="before" target="clearance_5"><template><div id="clearance_4">Other partial</div></template></turbo-stream>
|
231
|
+
# # to the stream named "identity:2:clearances"
|
232
|
+
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
|
233
|
+
# partial: "clearances/other_partial", locals: { a: 1 }
|
234
|
+
def broadcast_before_to(*streamables, target:, **rendering)
|
235
|
+
Turbo::StreamsChannel.broadcast_before_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
|
236
|
+
end
|
237
|
+
|
238
|
+
# Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
|
239
|
+
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
|
240
|
+
# appending named arguments to the call. Examples:
|
241
|
+
#
|
242
|
+
# # Sends <turbo-stream action="after" target="clearance_5"><template><div id="clearance_6">My Clearance</div></template></turbo-stream>
|
243
|
+
# # to the stream named "identity:2:clearances"
|
244
|
+
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5"
|
245
|
+
#
|
246
|
+
# # Sends <turbo-stream action="after" target="clearance_5"><template><div id="clearance_6">Other partial</div></template></turbo-stream>
|
247
|
+
# # to the stream named "identity:2:clearances"
|
248
|
+
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
|
249
|
+
# partial: "clearances/other_partial", locals: { a: 1 }
|
250
|
+
def broadcast_after_to(*streamables, target:, **rendering)
|
251
|
+
Turbo::StreamsChannel.broadcast_after_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
|
252
|
+
end
|
253
|
+
|
101
254
|
# Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
|
102
255
|
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
|
103
256
|
# appending named arguments to the call. Examples:
|
@@ -111,7 +264,7 @@ module Turbo::Broadcastable
|
|
111
264
|
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
|
112
265
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
113
266
|
def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
|
114
|
-
Turbo::StreamsChannel.broadcast_append_to
|
267
|
+
Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
115
268
|
end
|
116
269
|
|
117
270
|
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -132,32 +285,40 @@ module Turbo::Broadcastable
|
|
132
285
|
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
|
133
286
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
134
287
|
def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
|
135
|
-
Turbo::StreamsChannel.broadcast_prepend_to
|
288
|
+
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
136
289
|
end
|
137
290
|
|
138
|
-
# Same as <tt>#
|
291
|
+
# Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
|
139
292
|
def broadcast_prepend(target: broadcast_target_default, **rendering)
|
140
293
|
broadcast_prepend_to self, target: target, **rendering
|
141
294
|
end
|
142
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
|
+
|
143
304
|
# Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
|
144
305
|
#
|
145
306
|
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
146
307
|
# # to the stream named "identity:2:clearances"
|
147
308
|
# clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
|
148
|
-
def broadcast_action_to(*streamables, action:, target: broadcast_target_default, **rendering)
|
149
|
-
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?
|
150
311
|
end
|
151
312
|
|
152
313
|
# Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
|
153
|
-
def broadcast_action(action, target: broadcast_target_default, **rendering)
|
154
|
-
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
|
155
316
|
end
|
156
317
|
|
157
318
|
|
158
319
|
# Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
159
320
|
def broadcast_replace_later_to(*streamables, **rendering)
|
160
|
-
Turbo::StreamsChannel.broadcast_replace_later_to
|
321
|
+
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
161
322
|
end
|
162
323
|
|
163
324
|
# Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -165,9 +326,19 @@ module Turbo::Broadcastable
|
|
165
326
|
broadcast_replace_later_to self, **rendering
|
166
327
|
end
|
167
328
|
|
329
|
+
# Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
330
|
+
def broadcast_update_later_to(*streamables, **rendering)
|
331
|
+
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
332
|
+
end
|
333
|
+
|
334
|
+
# Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
|
335
|
+
def broadcast_update_later(**rendering)
|
336
|
+
broadcast_update_later_to self, **rendering
|
337
|
+
end
|
338
|
+
|
168
339
|
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
169
340
|
def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
|
170
|
-
Turbo::StreamsChannel.broadcast_append_later_to
|
341
|
+
Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
171
342
|
end
|
172
343
|
|
173
344
|
# Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -177,7 +348,7 @@ module Turbo::Broadcastable
|
|
177
348
|
|
178
349
|
# Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
179
350
|
def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering)
|
180
|
-
Turbo::StreamsChannel.broadcast_prepend_later_to
|
351
|
+
Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
181
352
|
end
|
182
353
|
|
183
354
|
# Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -185,19 +356,25 @@ module Turbo::Broadcastable
|
|
185
356
|
broadcast_prepend_later_to self, target: target, **rendering
|
186
357
|
end
|
187
358
|
|
188
|
-
|
189
|
-
|
190
|
-
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
|
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?
|
191
361
|
end
|
192
362
|
|
193
|
-
|
194
|
-
|
195
|
-
|
363
|
+
def broadcast_refresh_later
|
364
|
+
broadcast_refresh_later_to self
|
365
|
+
end
|
366
|
+
|
367
|
+
# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
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?
|
196
370
|
end
|
197
371
|
|
372
|
+
# Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
|
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
|
375
|
+
end
|
198
376
|
|
199
|
-
# Render a turbo stream template
|
200
|
-
# <tt>Turbo::Streams::BroadcastJob</tt>. Example:
|
377
|
+
# Render a turbo stream template with this broadcastable model passed as the local variable. Example:
|
201
378
|
#
|
202
379
|
# # Template: entries/_entry.turbo_stream.erb
|
203
380
|
# <%= turbo_stream.remove entry %>
|
@@ -209,27 +386,58 @@ module Turbo::Broadcastable
|
|
209
386
|
# <turbo-stream action="remove" target="entry_5"></turbo-stream>
|
210
387
|
# <turbo-stream action="append" target="entries"><template><div id="entry_5">My Entry</div></template></turbo-stream>
|
211
388
|
#
|
212
|
-
# ...to the stream named "entry:5"
|
389
|
+
# ...to the stream named "entry:5".
|
390
|
+
#
|
391
|
+
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
|
392
|
+
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
|
393
|
+
# be using `broadcast_render_later`, unless you specifically know why synchronous rendering is needed.
|
394
|
+
def broadcast_render(**rendering)
|
395
|
+
broadcast_render_to self, **rendering
|
396
|
+
end
|
397
|
+
|
398
|
+
# Same as <tt>broadcast_render</tt> but run with the added option of naming the stream using the passed
|
399
|
+
# <tt>streamables</tt>.
|
400
|
+
#
|
401
|
+
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
|
402
|
+
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
|
403
|
+
# be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
|
404
|
+
def broadcast_render_to(*streamables, **rendering)
|
405
|
+
Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
406
|
+
end
|
407
|
+
|
408
|
+
# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
213
409
|
def broadcast_render_later(**rendering)
|
214
410
|
broadcast_render_later_to self, **rendering
|
215
411
|
end
|
216
412
|
|
217
|
-
# Same as <tt>
|
413
|
+
# Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
|
218
414
|
# <tt>streamables</tt>.
|
219
415
|
def broadcast_render_later_to(*streamables, **rendering)
|
220
|
-
Turbo::StreamsChannel.broadcast_render_later_to
|
416
|
+
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
|
221
417
|
end
|
222
418
|
|
223
419
|
|
224
420
|
private
|
225
421
|
def broadcast_target_default
|
226
|
-
|
422
|
+
self.class.broadcast_target_default
|
227
423
|
end
|
228
424
|
|
229
425
|
def broadcast_rendering_with_defaults(options)
|
230
426
|
options.tap do |o|
|
231
|
-
|
232
|
-
|
427
|
+
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
428
|
+
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
|
429
|
+
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
|
430
|
+
|
431
|
+
if o[:html] || o[:partial]
|
432
|
+
return o
|
433
|
+
elsif o[:template] || o[:renderable]
|
434
|
+
o[:layout] = false
|
435
|
+
elsif o[:render] == false
|
436
|
+
return o
|
437
|
+
else
|
438
|
+
# if none of these options are passed in, it will set a partial from #to_partial_path
|
439
|
+
o[:partial] ||= to_partial_path
|
440
|
+
end
|
233
441
|
end
|
234
442
|
end
|
235
443
|
end
|
@@ -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
|