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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +113 -18
  3. data/Rakefile +19 -2
  4. data/app/assets/javascripts/turbo.js +4143 -1431
  5. data/app/assets/javascripts/turbo.min.js +29 -0
  6. data/app/assets/javascripts/turbo.min.js.map +1 -0
  7. data/app/channels/turbo/streams/broadcasts.rb +58 -22
  8. data/app/channels/turbo/streams/stream_name.rb +7 -0
  9. data/app/channels/turbo/streams_channel.rb +30 -2
  10. data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
  11. data/app/controllers/turbo/frames/frame_request.rb +22 -8
  12. data/app/controllers/turbo/native/navigation.rb +19 -9
  13. data/app/helpers/turbo/drive_helper.rb +75 -3
  14. data/app/helpers/turbo/frames_helper.rb +21 -2
  15. data/app/helpers/turbo/includes_helper.rb +2 -0
  16. data/app/helpers/turbo/streams/action_helper.rb +34 -9
  17. data/app/helpers/turbo/streams_helper.rb +20 -7
  18. data/app/javascript/turbo/cable.js +6 -3
  19. data/app/javascript/turbo/cable_stream_source_element.js +19 -3
  20. data/app/javascript/turbo/fetch_requests.js +59 -0
  21. data/app/javascript/turbo/index.js +6 -0
  22. data/app/javascript/turbo/snakeize.js +31 -0
  23. data/app/jobs/turbo/streams/action_broadcast_job.rb +4 -2
  24. data/app/jobs/turbo/streams/broadcast_job.rb +2 -0
  25. data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
  26. data/app/models/concerns/turbo/broadcastable.rb +246 -38
  27. data/app/models/turbo/debouncer.rb +24 -0
  28. data/app/models/turbo/streams/tag_builder.rb +163 -21
  29. data/app/models/turbo/thread_debouncer.rb +28 -0
  30. data/app/views/layouts/turbo_rails/frame.html.erb +8 -0
  31. data/config/routes.rb +1 -2
  32. data/lib/install/turbo_needs_redis.rb +20 -0
  33. data/lib/install/turbo_with_bun.rb +9 -0
  34. data/lib/install/turbo_with_importmap.rb +5 -0
  35. data/lib/install/turbo_with_node.rb +9 -0
  36. data/lib/tasks/turbo_tasks.rake +50 -8
  37. data/lib/turbo/broadcastable/test_helper.rb +172 -0
  38. data/lib/turbo/engine.rb +40 -6
  39. data/lib/turbo/test_assertions/integration_test_assertions.rb +76 -0
  40. data/lib/turbo/test_assertions.rb +69 -8
  41. data/lib/turbo/version.rb +1 -1
  42. data/lib/turbo-rails.rb +12 -0
  43. metadata +48 -173
  44. data/.github/workflows/ci.yml +0 -30
  45. data/.gitignore +0 -2
  46. data/Gemfile +0 -6
  47. data/Gemfile.lock +0 -147
  48. data/lib/install/turbo_with_asset_pipeline.rb +0 -20
  49. data/lib/install/turbo_with_webpacker.rb +0 -24
  50. data/package.json +0 -47
  51. data/rollup.config.js +0 -23
  52. data/test/drive/drive_helper_test.rb +0 -8
  53. data/test/dummy/.babelrc +0 -18
  54. data/test/dummy/.gitignore +0 -3
  55. data/test/dummy/.postcssrc.yml +0 -3
  56. data/test/dummy/Rakefile +0 -6
  57. data/test/dummy/app/assets/config/manifest.js +0 -2
  58. data/test/dummy/app/assets/images/.keep +0 -0
  59. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  60. data/test/dummy/app/assets/stylesheets/scaffold.css +0 -80
  61. data/test/dummy/app/channels/application_cable/channel.rb +0 -4
  62. data/test/dummy/app/channels/application_cable/connection.rb +0 -4
  63. data/test/dummy/app/controllers/application_controller.rb +0 -2
  64. data/test/dummy/app/controllers/concerns/.keep +0 -0
  65. data/test/dummy/app/controllers/messages_controller.rb +0 -12
  66. data/test/dummy/app/controllers/trays_controller.rb +0 -17
  67. data/test/dummy/app/helpers/application_helper.rb +0 -2
  68. data/test/dummy/app/javascript/packs/application.js +0 -0
  69. data/test/dummy/app/jobs/application_job.rb +0 -2
  70. data/test/dummy/app/mailboxes/application_mailbox.rb +0 -2
  71. data/test/dummy/app/mailboxes/messages_mailbox.rb +0 -4
  72. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  73. data/test/dummy/app/models/application_record.rb +0 -3
  74. data/test/dummy/app/models/concerns/.keep +0 -0
  75. data/test/dummy/app/models/message.rb +0 -29
  76. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  77. data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
  78. data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
  79. data/test/dummy/app/views/messages/_message.html.erb +0 -1
  80. data/test/dummy/app/views/messages/_message.turbo_stream.erb +0 -1
  81. data/test/dummy/app/views/messages/show.turbo_stream.erb +0 -9
  82. data/test/dummy/app/views/trays/index.html.erb +0 -3
  83. data/test/dummy/app/views/trays/show.html.erb +0 -3
  84. data/test/dummy/bin/bundle +0 -3
  85. data/test/dummy/bin/rails +0 -4
  86. data/test/dummy/bin/rake +0 -4
  87. data/test/dummy/bin/setup +0 -36
  88. data/test/dummy/bin/update +0 -31
  89. data/test/dummy/bin/yarn +0 -11
  90. data/test/dummy/config/application.rb +0 -22
  91. data/test/dummy/config/boot.rb +0 -5
  92. data/test/dummy/config/cable.yml +0 -10
  93. data/test/dummy/config/environment.rb +0 -5
  94. data/test/dummy/config/environments/development.rb +0 -34
  95. data/test/dummy/config/environments/production.rb +0 -96
  96. data/test/dummy/config/environments/test.rb +0 -38
  97. data/test/dummy/config/initializers/application_controller_renderer.rb +0 -8
  98. data/test/dummy/config/initializers/assets.rb +0 -14
  99. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  100. data/test/dummy/config/initializers/content_security_policy.rb +0 -22
  101. data/test/dummy/config/initializers/cookies_serializer.rb +0 -5
  102. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  103. data/test/dummy/config/initializers/inflections.rb +0 -16
  104. data/test/dummy/config/initializers/mime_types.rb +0 -4
  105. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  106. data/test/dummy/config/locales/en.yml +0 -33
  107. data/test/dummy/config/puma.rb +0 -34
  108. data/test/dummy/config/routes.rb +0 -4
  109. data/test/dummy/config/spring.rb +0 -6
  110. data/test/dummy/config/webpack/development.js +0 -3
  111. data/test/dummy/config/webpack/environment.js +0 -3
  112. data/test/dummy/config/webpack/production.js +0 -3
  113. data/test/dummy/config/webpack/test.js +0 -3
  114. data/test/dummy/config/webpacker.yml +0 -65
  115. data/test/dummy/config.ru +0 -5
  116. data/test/dummy/lib/assets/.keep +0 -0
  117. data/test/dummy/log/.keep +0 -0
  118. data/test/dummy/public/404.html +0 -67
  119. data/test/dummy/public/422.html +0 -67
  120. data/test/dummy/public/500.html +0 -66
  121. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  122. data/test/dummy/public/apple-touch-icon.png +0 -0
  123. data/test/dummy/public/favicon.ico +0 -0
  124. data/test/frames/frame_request_controller_test.rb +0 -21
  125. data/test/frames/frames_helper_test.rb +0 -21
  126. data/test/native/navigation_controller_test.rb +0 -42
  127. data/test/streams/broadcastable_test.rb +0 -80
  128. data/test/streams/streams_channel_test.rb +0 -105
  129. data/test/streams/streams_controller_test.rb +0 -29
  130. data/test/turbo_test.rb +0 -10
  131. data/turbo-rails.gemspec +0 -17
  132. 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
+ }
@@ -5,3 +5,9 @@ export { Turbo }
5
5
 
6
6
  import * as cable from "./cable"
7
7
  export { cable }
8
+
9
+ import { encodeMethodIntoRequestBody } from "./fetch_requests"
10
+
11
+ window.Turbo = Turbo
12
+
13
+ addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)
@@ -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
- def perform(stream, action:, target:, **rendering)
4
- Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering
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
@@ -0,0 +1,7 @@
1
+ class Turbo::Streams::BroadcastStreamJob < ActiveJob::Base
2
+ discard_on ActiveJob::DeserializationError
3
+
4
+ def perform(stream, content:)
5
+ Turbo::StreamsChannel.broadcast_stream_to(stream, content: content)
6
+ end
7
+ 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>broadcast_render_later</tt> or
35
- # <tt>broadcast_render_later_to</tt> to render a turbo stream template with multiple actions.
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>insertion</tt> and
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
- def broadcasts_to(stream, inserts_by: :append, target: model_name.plural)
55
- after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target }
56
- after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream) }
57
- after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) }
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 the current model.
61
- def broadcasts(inserts_by: :append, target: model_name.plural)
62
- after_create_commit -> { broadcast_action_later action: inserts_by, target: target }
63
- after_update_commit -> { broadcast_replace_later }
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 *streamables, target: self
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 *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?
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 *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?
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 *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?
136
289
  end
137
290
 
138
- # Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
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 *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?
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 *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?
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 *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?
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
- # Same as <tt>broadcast_action_later_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
189
- def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, **rendering)
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
- # Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
194
- def broadcast_action_later(action:, target: broadcast_target_default, **rendering)
195
- broadcast_action_later_to self, action: action, target: target, **rendering
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 asynchronously with this broadcastable model passed as the local variable using a
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>broadcast_prepend_to</tt> but run with the added option of naming the stream using the passed
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 *streamables, **broadcast_rendering_with_defaults(rendering)
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
- model_name.plural
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
- o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.singular.to_sym => self)
232
- o[:partial] ||= to_partial_path
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