turbo-rails 2.0.6 → 2.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,7 +6,7 @@ module Turbo::Streams::Broadcasts
6
6
  include Turbo::Streams::ActionHelper
7
7
 
8
8
  def broadcast_remove_to(*streamables, **opts)
9
- broadcast_action_to(*streamables, action: :remove, **opts)
9
+ broadcast_action_to(*streamables, action: :remove, render: false, **opts)
10
10
  end
11
11
 
12
12
  def broadcast_replace_to(*streamables, **opts)
@@ -38,10 +38,9 @@ module Turbo::Streams::Broadcasts
38
38
  end
39
39
 
40
40
  def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
41
- broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
42
- rendering.delete(:content) || rendering.delete(:html) || (rendering[:render] != false && rendering.any? ? render_format(:html, **rendering) : nil),
43
- **attributes
44
- ))
41
+ broadcast_stream_to *streamables, content: turbo_stream_action_tag(
42
+ action, target: target, targets: targets, template: render_broadcast_action(rendering), **attributes
43
+ )
45
44
  end
46
45
 
47
46
  def broadcast_replace_later_to(*streamables, **opts)
@@ -70,7 +69,7 @@ module Turbo::Streams::Broadcasts
70
69
 
71
70
  def broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **opts)
72
71
  refresh_debouncer_for(*streamables, request_id: request_id).debounce do
73
- Turbo::Streams::BroadcastStreamJob.perform_later stream_name_from(streamables), content: turbo_stream_refresh_tag(request_id: request_id, **opts)
72
+ Turbo::Streams::BroadcastStreamJob.perform_later stream_name_from(streamables), content: turbo_stream_refresh_tag(request_id: request_id, **opts).to_str # Sidekiq requires job arguments to be valid JSON types, such as String
74
73
  end
75
74
  end
76
75
 
@@ -111,4 +110,16 @@ module Turbo::Streams::Broadcasts
111
110
  def render_format(format, **rendering)
112
111
  ApplicationController.render(formats: [ format ], **rendering)
113
112
  end
113
+
114
+ def render_broadcast_action(rendering)
115
+ content = rendering.delete(:content)
116
+ html = rendering.delete(:html)
117
+ render = rendering.delete(:render)
118
+
119
+ if render == false
120
+ nil
121
+ else
122
+ content || html || (render_format(:html, **rendering) if rendering.present?)
123
+ end
124
+ end
114
125
  end
@@ -48,7 +48,13 @@ module Turbo::StreamsHelper
48
48
  # It is also possible to pass additional parameters to the channel by passing them through `data` attributes:
49
49
  #
50
50
  # <%= turbo_stream_from "room", channel: RoomChannel, data: {room_name: "room #1"} %>
51
+ #
52
+ # Raises an +ArgumentError+ if all streamables are blank
53
+ #
54
+ # <%= turbo_stream_from("") %> # => ArgumentError: streamables can't be blank
55
+ # <%= turbo_stream_from("", nil) %> # => ArgumentError: streamables can't be blank
51
56
  def turbo_stream_from(*streamables, **attributes)
57
+ raise ArgumentError, "streamables can't be blank" unless streamables.any?(&:present?)
52
58
  attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
53
59
  attributes[:"signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables)
54
60
 
@@ -3,6 +3,8 @@ import { subscribeTo } from "./cable"
3
3
  import snakeize from "./snakeize"
4
4
 
5
5
  class TurboCableStreamSourceElement extends HTMLElement {
6
+ static observedAttributes = ["channel", "signed-stream-name"]
7
+
6
8
  async connectedCallback() {
7
9
  connectStreamSource(this)
8
10
  this.subscription = await subscribeTo(this.channel, {
@@ -15,6 +17,14 @@ class TurboCableStreamSourceElement extends HTMLElement {
15
17
  disconnectedCallback() {
16
18
  disconnectStreamSource(this)
17
19
  if (this.subscription) this.subscription.unsubscribe()
20
+ this.subscriptionDisconnected()
21
+ }
22
+
23
+ attributeChangedCallback() {
24
+ if (this.subscription) {
25
+ this.disconnectedCallback()
26
+ this.connectedCallback()
27
+ }
18
28
  }
19
29
 
20
30
  dispatchMessageEvent(data) {
@@ -1,4 +1,4 @@
1
- # Turbo streams can be broadcasted directly from models that include this module (this is automatically done for Active Records).
1
+ # Turbo streams can be broadcasted directly from models that include this module (this is automatically done for Active Records if ActiveJob is loaded).
2
2
  # This makes it convenient to execute both synchronous and asynchronous updates, and render directly from callbacks in models
3
3
  # or from controllers or jobs that act on those models. Here's an example:
4
4
  #
@@ -241,13 +241,13 @@ module Turbo::Broadcastable
241
241
  #
242
242
  # # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
243
243
  # clearance.broadcast_remove_to examiner.identity, :clearances
244
- def broadcast_remove_to(*streamables, target: self)
245
- Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
244
+ def broadcast_remove_to(*streamables, target: self, **rendering)
245
+ Turbo::StreamsChannel.broadcast_remove_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
246
246
  end
247
247
 
248
248
  # Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
249
- def broadcast_remove
250
- broadcast_remove_to self
249
+ def broadcast_remove(**rendering)
250
+ broadcast_remove_to self, **rendering
251
251
  end
252
252
 
253
253
  # Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed
@@ -265,7 +265,7 @@ module Turbo::Broadcastable
265
265
  # # to the stream named "identity:2:clearances"
266
266
  # clearance.broadcast_replace_to examiner.identity, :clearance, attributes: { method: :morph }, partial: "clearances/other_partial", locals: { a: 1 }
267
267
  def broadcast_replace_to(*streamables, **rendering)
268
- Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
268
+ Turbo::StreamsChannel.broadcast_replace_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
269
269
  end
270
270
 
271
271
  # Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
@@ -288,7 +288,7 @@ module Turbo::Broadcastable
288
288
  # # to the stream named "identity:2:clearances"
289
289
  # # clearance.broadcast_update_to examiner.identity, :clearances, attributes: { method: :morph }, partial: "clearances/other_partial", locals: { a: 1 }
290
290
  def broadcast_update_to(*streamables, **rendering)
291
- Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
291
+ Turbo::StreamsChannel.broadcast_update_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
292
292
  end
293
293
 
294
294
  # Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
@@ -308,8 +308,10 @@ module Turbo::Broadcastable
308
308
  # # to the stream named "identity:2:clearances"
309
309
  # clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
310
310
  # partial: "clearances/other_partial", locals: { a: 1 }
311
- def broadcast_before_to(*streamables, target:, **rendering)
312
- Turbo::StreamsChannel.broadcast_before_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
311
+ def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
312
+ raise ArgumentError, "at least one of target or targets is required" unless target || targets
313
+
314
+ Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
313
315
  end
314
316
 
315
317
  # Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
@@ -324,8 +326,10 @@ module Turbo::Broadcastable
324
326
  # # to the stream named "identity:2:clearances"
325
327
  # clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
326
328
  # partial: "clearances/other_partial", locals: { a: 1 }
327
- def broadcast_after_to(*streamables, target:, **rendering)
328
- Turbo::StreamsChannel.broadcast_after_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
329
+ def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
330
+ raise ArgumentError, "at least one of target or targets is required" unless target || targets
331
+
332
+ Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
329
333
  end
330
334
 
331
335
  # Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
@@ -341,7 +345,7 @@ module Turbo::Broadcastable
341
345
  # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
342
346
  # partial: "clearances/other_partial", locals: { a: 1 }
343
347
  def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
344
- Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
348
+ Turbo::StreamsChannel.broadcast_append_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
345
349
  end
346
350
 
347
351
  # Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
@@ -362,7 +366,7 @@ module Turbo::Broadcastable
362
366
  # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
363
367
  # partial: "clearances/other_partial", locals: { a: 1 }
364
368
  def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
365
- Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
369
+ Turbo::StreamsChannel.broadcast_prepend_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
366
370
  end
367
371
 
368
372
  # Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
@@ -389,7 +393,7 @@ module Turbo::Broadcastable
389
393
  # # to the stream named "identity:2:clearances"
390
394
  # clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
391
395
  def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
392
- Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
396
+ Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
393
397
  end
394
398
 
395
399
  # Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
@@ -399,7 +403,7 @@ module Turbo::Broadcastable
399
403
 
400
404
  # Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
401
405
  def broadcast_replace_later_to(*streamables, **rendering)
402
- Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
406
+ Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
403
407
  end
404
408
 
405
409
  # Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -409,7 +413,7 @@ module Turbo::Broadcastable
409
413
 
410
414
  # Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
411
415
  def broadcast_update_later_to(*streamables, **rendering)
412
- Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
416
+ Turbo::StreamsChannel.broadcast_update_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
413
417
  end
414
418
 
415
419
  # Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -419,7 +423,7 @@ module Turbo::Broadcastable
419
423
 
420
424
  # Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
421
425
  def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
422
- Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
426
+ Turbo::StreamsChannel.broadcast_append_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
423
427
  end
424
428
 
425
429
  # Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -429,7 +433,7 @@ module Turbo::Broadcastable
429
433
 
430
434
  # Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
431
435
  def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering)
432
- Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
436
+ Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
433
437
  end
434
438
 
435
439
  # Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -449,7 +453,7 @@ module Turbo::Broadcastable
449
453
 
450
454
  # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
451
455
  def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
452
- Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
456
+ Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
453
457
  end
454
458
 
455
459
  # Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -485,7 +489,7 @@ module Turbo::Broadcastable
485
489
  # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
486
490
  # be using +broadcast_render_later_to+, unless you specifically know why synchronous rendering is needed.
487
491
  def broadcast_render_to(*streamables, **rendering)
488
- Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
492
+ Turbo::StreamsChannel.broadcast_render_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
489
493
  end
490
494
 
491
495
  # Same as <tt>broadcast_render_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
@@ -496,7 +500,7 @@ module Turbo::Broadcastable
496
500
  # Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
497
501
  # <tt>streamables</tt>.
498
502
  def broadcast_render_later_to(*streamables, **rendering)
499
- Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
503
+ Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **extract_options_and_add_target(rendering)) unless suppressed_turbo_broadcasts?
500
504
  end
501
505
 
502
506
  private
@@ -504,11 +508,17 @@ module Turbo::Broadcastable
504
508
  self.class.broadcast_target_default
505
509
  end
506
510
 
511
+ def extract_options_and_add_target(rendering = {}, target: broadcast_target_default)
512
+ broadcast_rendering_with_defaults(rendering).tap do |options|
513
+ options[:target] = target if !options.key?(:target) && !options.key?(:targets)
514
+ end
515
+ end
516
+
507
517
  def broadcast_rendering_with_defaults(options)
508
518
  options.tap do |o|
509
519
  # Add the current instance into the locals with the element name (which is the un-namespaced name)
510
520
  # as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
511
- o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
521
+ o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self).compact
512
522
 
513
523
  if o[:html] || o[:partial]
514
524
  return o
@@ -77,8 +77,9 @@ class Turbo::Streams::TagBuilder
77
77
  # <%= turbo_stream.replace "clearance_5" do %>
78
78
  # <div id='clearance_5'>Replace the dom target identified by clearance_5</div>
79
79
  # <% end %>
80
- def replace(target, content = nil, **rendering, &block)
81
- action :replace, target, content, **rendering, &block
80
+ # <%= turbo_stream.replace clearance, "<div>Morph the dom target</div>", method: :morph %>
81
+ def replace(target, content = nil, method: nil, **rendering, &block)
82
+ action :replace, target, content, method: method, **rendering, &block
82
83
  end
83
84
 
84
85
  # Replace the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
@@ -90,8 +91,9 @@ class Turbo::Streams::TagBuilder
90
91
  # <%= turbo_stream.replace_all ".clearance_item" do %>
91
92
  # <div class='.clearance_item'>Replace the dom target identified by the class clearance_item</div>
92
93
  # <% end %>
93
- def replace_all(targets, content = nil, **rendering, &block)
94
- action_all :replace, targets, content, **rendering, &block
94
+ # <%= turbo_stream.replace_all clearance, "<div>Morph the dom target</div>", method: :morph %>
95
+ def replace_all(targets, content = nil, method: nil, **rendering, &block)
96
+ action_all :replace, targets, content, method: method, **rendering, &block
95
97
  end
96
98
 
97
99
  # Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
@@ -155,8 +157,9 @@ class Turbo::Streams::TagBuilder
155
157
  # <%= turbo_stream.update "clearance_5" do %>
156
158
  # Update the content of the dom target identified by clearance_5
157
159
  # <% end %>
158
- def update(target, content = nil, **rendering, &block)
159
- action :update, target, content, **rendering, &block
160
+ # <%= turbo_stream.update clearance, "<div>Morph the dom target</div>", method: :morph %>
161
+ def update(target, content = nil, method: nil, **rendering, &block)
162
+ action :update, target, content, method: method, **rendering, &block
160
163
  end
161
164
 
162
165
  # Update the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
@@ -168,8 +171,9 @@ class Turbo::Streams::TagBuilder
168
171
  # <%= turbo_stream.update_all "clearance_item" do %>
169
172
  # Update the content of the dom target identified by the class clearance_item
170
173
  # <% end %>
171
- def update_all(targets, content = nil, **rendering, &block)
172
- action_all :update, targets, content, **rendering, &block
174
+ # <%= turbo_stream.update_all clearance, "<div>Morph the dom target</div>", method: :morph %>
175
+ def update_all(targets, content = nil, method: nil, **rendering, &block)
176
+ action_all :update, targets, content, method: method, **rendering, &block
173
177
  end
174
178
 
175
179
  # Append to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
@@ -228,49 +232,37 @@ class Turbo::Streams::TagBuilder
228
232
  action_all :prepend, targets, content, **rendering, &block
229
233
  end
230
234
 
231
- # Morph the <tt>target</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
232
- # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
235
+ # Creates a `turbo-stream` tag with an `[action="refresh"`] attribute and a
236
+ # `[request-id]` attribute that defaults to `Turbo.current_request_id`:
233
237
  #
234
- # <%= turbo_stream.morph "clearance_5", "<div id='clearance_5'>Morph the dom target identified by clearance_5</div>" %>
235
- # <%= turbo_stream.morph clearance %>
236
- # <%= turbo_stream.morph clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
237
- # <%= turbo_stream.morph "clearance_5" do %>
238
- # <div id='clearance_5'>Morph the dom target identified by clearance_5</div>
239
- # <% end %>
240
- def morph(target, content = nil, **rendering, &block)
241
- action :morph, target, content, **rendering, &block
242
- end
243
-
244
- # Morph the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
245
- # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the targets as a record. Examples:
238
+ # turbo_stream.refresh
239
+ # # => <turbo-stream action="refresh" request-id="ef083d55-7516-41b1-ad28-16f553399c6a"></turbo-stream>
246
240
  #
247
- # <%= turbo_stream.morph_all ".clearance_item", "<div class='clearance_item'>Morph the dom target identified by the class clearance_item</div>" %>
248
- # <%= turbo_stream.morph_all clearance %>
249
- # <%= turbo_stream.morph_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
250
- # <%= turbo_stream.morph_all ".clearance_item" do %>
251
- # <div class='clearance_item'>Morph the dom target identified by the class clearance_item</div>
252
- # <% end %>
253
- def morph_all(targets, content = nil, **rendering, &block)
254
- action_all :morph, targets, content, **rendering, &block
241
+ # turbo_stream.refresh request_id: "abc123"
242
+ # # => <turbo-stream action="refresh" request-id="abc123"></turbo-stream>
243
+ def refresh(...)
244
+ turbo_stream_refresh_tag(...)
255
245
  end
256
246
 
257
247
  # Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
258
- def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
248
+ def action(name, target, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
259
249
  template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
260
250
 
261
- turbo_stream_action_tag name, target: target, template: template
251
+ turbo_stream_action_tag name, target: target, template: template, method: method
262
252
  end
263
253
 
264
254
  # Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
265
- def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
255
+ def action_all(name, targets, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
266
256
  template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
267
257
 
268
- turbo_stream_action_tag name, targets: targets, template: template
258
+ turbo_stream_action_tag name, targets: targets, template: template, method: method
269
259
  end
270
260
 
271
261
  private
272
262
  def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
273
263
  case
264
+ when target.respond_to?(:render_in) && content.nil?
265
+ target.render_in(@view_context, &block)
274
266
  when content.respond_to?(:render_in)
275
267
  content.render_in(@view_context, &block)
276
268
  when content
data/config/routes.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  Rails.application.routes.draw do
2
- get "recede_historical_location" => "turbo/native/navigation#recede", as: :turbo_recede_historical_location
3
- get "resume_historical_location" => "turbo/native/navigation#resume", as: :turbo_resume_historical_location
4
- get "refresh_historical_location" => "turbo/native/navigation#refresh", as: :turbo_refresh_historical_location
2
+ get "recede_historical_location", to: "turbo/native/navigation#recede", as: :turbo_recede_historical_location
3
+ get "resume_historical_location", to: "turbo/native/navigation#resume", as: :turbo_resume_historical_location
4
+ get "refresh_historical_location", to: "turbo/native/navigation#refresh", as: :turbo_refresh_historical_location
5
5
  end if Turbo.draw_routes
@@ -5,20 +5,6 @@ module Turbo
5
5
  system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
6
6
  end
7
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
8
  def using_bun?
23
9
  Rails.root.join("bun.config.js").exist?
24
10
  end
@@ -43,24 +29,16 @@ namespace :turbo do
43
29
  desc "Install Turbo into the app with asset pipeline"
44
30
  task :importmap do
45
31
  Turbo::Tasks.run_turbo_install_template "turbo_with_importmap"
46
- Turbo::Tasks.switch_on_redis_if_available
47
32
  end
48
33
 
49
34
  desc "Install Turbo into the app with webpacker"
50
35
  task :node do
51
36
  Turbo::Tasks.run_turbo_install_template "turbo_with_node"
52
- Turbo::Tasks.switch_on_redis_if_available
53
37
  end
54
38
 
55
39
  desc "Install Turbo into the app with bun"
56
40
  task :bun do
57
41
  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"
64
42
  end
65
43
  end
66
44
  end
data/lib/turbo/engine.rb CHANGED
@@ -5,6 +5,7 @@ module Turbo
5
5
  isolate_namespace Turbo
6
6
  config.eager_load_namespaces << Turbo
7
7
  config.turbo = ActiveSupport::OrderedOptions.new
8
+ config.turbo.test_connect_after_actions = %i[visit]
8
9
  config.autoload_once_paths = %W(
9
10
  #{root}/app/channels
10
11
  #{root}/app/controllers
@@ -15,6 +16,27 @@ module Turbo
15
16
  #{root}/app/jobs
16
17
  )
17
18
 
19
+ # If the parent application does not use Active Job, app/jobs cannot
20
+ # be eager loaded, because it references the ActiveJob constant.
21
+ #
22
+ # When turbo-rails depends on Rails 7 or above, the entire block can be
23
+ # reduced to
24
+ #
25
+ # unless defined?(ActiveJob)
26
+ # Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
27
+ # end
28
+ #
29
+ initializer "turbo.no_active_job", before: :set_eager_load_paths do
30
+ unless defined?(ActiveJob)
31
+ if Rails.autoloaders.zeitwerk_enabled?
32
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
33
+ else
34
+ # This else branch only runs in Rails 6.x + classic mode.
35
+ config.eager_load_paths.delete("#{root}/app/jobs")
36
+ end
37
+ end
38
+ end
39
+
18
40
  # If the parent application does not use Action Cable, app/channels cannot
19
41
  # be eager loaded, because it references the ActionCable constant.
20
42
  #
@@ -71,7 +93,9 @@ module Turbo
71
93
 
72
94
  initializer "turbo.broadcastable" do
73
95
  ActiveSupport.on_load(:active_record) do
74
- include Turbo::Broadcastable
96
+ if defined?(ActiveJob)
97
+ include Turbo::Broadcastable
98
+ end
75
99
  end
76
100
  end
77
101
 
@@ -101,8 +125,10 @@ module Turbo
101
125
 
102
126
  ActiveSupport.on_load(:action_cable) do
103
127
  ActiveSupport.on_load(:active_support_test_case) do
104
- require "turbo/broadcastable/test_helper"
105
- include Turbo::Broadcastable::TestHelper
128
+ if defined?(ActiveJob)
129
+ require "turbo/broadcastable/test_helper"
130
+ include Turbo::Broadcastable::TestHelper
131
+ end
106
132
  end
107
133
  end
108
134
 
@@ -126,5 +152,24 @@ module Turbo
126
152
  end
127
153
  end
128
154
  end
155
+
156
+ initializer "turbo.system_test_helper" do
157
+ ActiveSupport.on_load(:action_dispatch_system_test_case) do
158
+ require "turbo/system_test_helper"
159
+ include Turbo::SystemTestHelper
160
+ end
161
+ end
162
+
163
+ config.after_initialize do |app|
164
+ ActiveSupport.on_load(:action_dispatch_system_test_case) do
165
+ app.config.turbo.test_connect_after_actions.map do |method|
166
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
167
+ def #{method}(...) # def visit(...)
168
+ super.tap { connect_turbo_cable_stream_sources } # super.tap { connect_turbo_cable_stream_sources }
169
+ end # end
170
+ RUBY
171
+ end
172
+ end
173
+ end
129
174
  end
130
175
  end
@@ -0,0 +1,128 @@
1
+ module Turbo::SystemTestHelper
2
+ # Delay until every `<turbo-cable-stream-source>` element present in the page
3
+ # is ready to receive broadcasts
4
+ #
5
+ # test "renders broadcasted Messages" do
6
+ # message = Message.new content: "Hello, from Action Cable"
7
+ #
8
+ # visit "/"
9
+ # click_link "All Messages"
10
+ # message.save! # execute server-side code to broadcast a Message
11
+ #
12
+ # assert_text message.content
13
+ # end
14
+ #
15
+ # By default, calls to `#visit` will wait for all `<turbo-cable-stream-source>`
16
+ # elements to connect. You can control this by modifying the
17
+ # `config.turbo.test_connect_after_actions`. For example, to wait after calls to
18
+ # `#click_link`, add the following to `config/environments/test.rb`:
19
+ #
20
+ # # config/environments/test.rb
21
+ # config.turbo.test_connect_after_actions << :click_link
22
+ #
23
+ # To disable automatic connecting, set the configuration to `[]`:
24
+ #
25
+ # # config/environments/test.rb
26
+ # config.turbo.test_connect_after_actions = []
27
+ #
28
+ def connect_turbo_cable_stream_sources(**options, &block)
29
+ all(:turbo_cable_stream_source, **options, connected: false, wait: 0).each do |element|
30
+ element.assert_matches_selector(:turbo_cable_stream_source, **options, connected: true, &block)
31
+ end
32
+ end
33
+
34
+ # Asserts that a `<turbo-cable-stream-source>` element is present in the
35
+ # document
36
+ #
37
+ # ==== Arguments
38
+ #
39
+ # * <tt>locator</tt> optional locator to determine the element's
40
+ # `[signed-stream-name]` attribute. Can be of any type that is a valid
41
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
42
+ #
43
+ # ==== Options
44
+ #
45
+ # * <tt>:connected</tt> matches the `[connected]` attribute
46
+ # * <tt>:channel</tt> matches the `[channel]` attribute. Can be a Class,
47
+ # String, Symbol, or Regexp
48
+ # * <tt>:signed_stream_name</tt> matches the element's `[signed-stream-name]`
49
+ # attribute. Can be of any type that is a valid
50
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
51
+ #
52
+ # In addition to the filters listed above, accepts any valid Capybara global
53
+ # filter option.
54
+ def assert_turbo_cable_stream_source(...)
55
+ assert_selector(:turbo_cable_stream_source, ...)
56
+ end
57
+
58
+ # Asserts that a `<turbo-cable-stream-source>` element is absent from the
59
+ # document
60
+ #
61
+ # ==== Arguments
62
+ #
63
+ # * <tt>locator</tt> optional locator to determine the element's
64
+ # `[signed-stream-name]` attribute. Can be of any type that is a valid
65
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
66
+ #
67
+ # ==== Options
68
+ #
69
+ # * <tt>:connected</tt> matches the `[connected]` attribute
70
+ # * <tt>:channel</tt> matches the `[channel]` attribute. Can be a Class,
71
+ # String, Symbol, or Regexp
72
+ # * <tt>:signed_stream_name</tt> matches the element's `[signed-stream-name]`
73
+ # attribute. Can be of any type that is a valid
74
+ # argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
75
+ #
76
+ # In addition to the filters listed above, accepts any valid Capybara global
77
+ # filter option.
78
+ def assert_no_turbo_cable_stream_source(...)
79
+ assert_no_selector(:turbo_cable_stream_source, ...)
80
+ end
81
+
82
+ Capybara.add_selector :turbo_cable_stream_source do
83
+ xpath do |locator|
84
+ xpath = XPath.descendant.where(XPath.local_name == "turbo-cable-stream-source")
85
+ xpath.where(SignedStreamNameConditions.new(locator).reduce(:|))
86
+ end
87
+
88
+ expression_filter :connected do |xpath, value|
89
+ builder(xpath).add_attribute_conditions(connected: value)
90
+ end
91
+
92
+ expression_filter :channel do |xpath, value|
93
+ builder(xpath).add_attribute_conditions(channel: value.try(:name) || value)
94
+ end
95
+
96
+ expression_filter :signed_stream_name do |xpath, value|
97
+ case value
98
+ when TrueClass, FalseClass, NilClass, Regexp
99
+ builder(xpath).add_attribute_conditions("signed-stream-name": value)
100
+ else
101
+ xpath.where(SignedStreamNameConditions.new(value).reduce(:|))
102
+ end
103
+ end
104
+ end
105
+
106
+ class SignedStreamNameConditions # :nodoc:
107
+ include Turbo::Streams::StreamName, Enumerable
108
+
109
+ def initialize(value)
110
+ @value = value
111
+ end
112
+
113
+ def attribute
114
+ XPath.attr(:"signed-stream-name")
115
+ end
116
+
117
+ def each
118
+ if @value.is_a?(String)
119
+ yield attribute == @value
120
+ yield attribute == signed_stream_name(@value)
121
+ elsif @value.is_a?(Array) || @value.respond_to?(:to_key)
122
+ yield attribute == signed_stream_name(@value)
123
+ elsif @value.present?
124
+ yield attribute == @value
125
+ end
126
+ end
127
+ end
128
+ end
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "2.0.6"
2
+ VERSION = "2.0.10"
3
3
  end