turbo-rails 1.0.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,7 @@ module Turbo::Streams::Broadcasts
35
35
 
36
36
  def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
37
37
  broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
38
- rendering.delete(:content) || (rendering.any? ? render_format(:html, **rendering) : nil)
38
+ rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil)
39
39
  ))
40
40
  end
41
41
 
@@ -1,19 +1,27 @@
1
1
  # Turbo frame requests are requests made from within a turbo frame with the intention of replacing the content of just
2
2
  # that frame, not the whole page. They are automatically tagged as such by the Turbo Frame JavaScript, which adds a
3
- # <tt>Turbo-Frame</tt> header to the request. When that header is detected by the controller, we ensure that any
4
- # template layout is skipped (since we're only working on an in-page frame, thus can skip the weight of the layout), and
5
- # that the etag for the page is changed (such that a cache for a layout-less request isn't served on a normal request
6
- # and vice versa).
3
+ # <tt>Turbo-Frame</tt> header to the request.
7
4
  #
8
- # This is merely a rendering optimization. Everything would still work just fine if we rendered everything including the layout.
9
- # Turbo Frames knows how to fish out the relevant frame regardless.
5
+ # When that header is detected by the controller, we substitute our own minimal layout in place of the
6
+ # application-supplied layout (since we're only working on an in-page frame, thus can skip the weight of the layout). We
7
+ # use a minimal layout, rather than avoid the layout entirely, so that it's still possible to render content into the
8
+ # <tt>head<tt>.
9
+ #
10
+ # Accordingly, we ensure that the etag for the page is changed, such that a cache for a minimal-layout request isn't
11
+ # served on a normal request and vice versa.
12
+ #
13
+ # This is merely a rendering optimization. Everything would still work just fine if we rendered everything including the
14
+ # full layout. Turbo Frames knows how to fish out the relevant frame regardless.
15
+ #
16
+ # The layout used is <tt>turbo_rails/frame.html.erb</tt>. If there's a need to customize this layout, an application can
17
+ # supply its own (such as <tt>app/views/layouts/turbo_rails/frame.html.erb</tt>) which will be used instead.
10
18
  #
11
19
  # This module is automatically included in <tt>ActionController::Base</tt>.
12
20
  module Turbo::Frames::FrameRequest
13
21
  extend ActiveSupport::Concern
14
22
 
15
23
  included do
16
- layout -> { false if turbo_frame_request? }
24
+ layout -> { "turbo_rails/frame" if turbo_frame_request? }
17
25
  etag { :frame if turbo_frame_request? }
18
26
  end
19
27
 
@@ -33,8 +33,10 @@ module Turbo::Native::Navigation
33
33
 
34
34
  # :nodoc:
35
35
  def turbo_native_action_or_redirect(url, action, redirect_type, options = {})
36
+ native_params = options.delete(:native_params) || {}
37
+
36
38
  if turbo_native_app?
37
- redirect_to send("turbo_#{action}_historical_location_url", notice: options[:notice] || options.delete(:native_notice))
39
+ redirect_to send("turbo_#{action}_historical_location_url", notice: options[:notice], **native_params)
38
40
  elsif redirect_type == :back
39
41
  redirect_back fallback_location: url, **options
40
42
  else
@@ -1,6 +1,5 @@
1
1
  module Turbo::DriveHelper
2
- # Pages that are more likely than not to be a cache miss can skip turbo cache to avoid visual jitter.
3
- # Note: This requires a +yield :head+ provision in the application layout.
2
+ # Note: These helpers require a +yield :head+ provision in the layout.
4
3
  #
5
4
  # ==== Example
6
5
  #
@@ -10,7 +9,21 @@ module Turbo::DriveHelper
10
9
  # # app/views/trays/index.html.erb
11
10
  # <% turbo_exempts_page_from_cache %>
12
11
  # <p>Page that shouldn't be cached by Turbo</p>
12
+
13
+ # Pages that are more likely than not to be a cache miss can skip turbo cache to avoid visual jitter.
14
+ # Cannot be used along with +turbo_exempts_page_from_preview+.
13
15
  def turbo_exempts_page_from_cache
14
- provide :head, %(<meta name="turbo-cache-control" content="no-cache">).html_safe
16
+ provide :head, tag.meta(name: "turbo-cache-control", content: "no-cache")
17
+ end
18
+
19
+ # Specify that a cached version of the page should not be shown as a preview during an application visit.
20
+ # Cannot be used along with +turbo_exempts_page_from_cache+.
21
+ def turbo_exempts_page_from_preview
22
+ provide :head, tag.meta(name: "turbo-cache-control", content: "no-preview")
23
+ end
24
+
25
+ # Force the page, when loaded by Turbo, to be cause a full page reload.
26
+ def turbo_page_requires_reload
27
+ provide :head, tag.meta(name: "turbo-visit-control", content: "reload")
15
28
  end
16
29
  end
@@ -23,8 +23,20 @@ module Turbo::FramesHelper
23
23
  # <div>My tray frame!</div>
24
24
  # <% end %>
25
25
  # # => <turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
26
- def turbo_frame_tag(id, src: nil, target: nil, **attributes, &block)
27
- id = id.respond_to?(:to_key) ? dom_id(id) : id
26
+ #
27
+ # The `turbo_frame_tag` helper will convert the arguments it receives to their
28
+ # `dom_id` if applicable to easily generate unique ids for Turbo Frames:
29
+ #
30
+ # <%= turbo_frame_tag(Article.find(1)) %>
31
+ # # => <turbo-frame id="article_1"></turbo-frame>
32
+ #
33
+ # <%= turbo_frame_tag(Article.find(1), "comments") %>
34
+ # # => <turbo-frame id="article_1_comments"></turbo-frame>
35
+ #
36
+ # <%= turbo_frame_tag(Article.find(1), Comment.new) %>
37
+ # # => <turbo-frame id="article_1_new_comment"></turbo-frame>
38
+ def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
39
+ id = ids.map { |id| id.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(id) : id }.join("_")
28
40
  src = url_for(src) if src.present?
29
41
 
30
42
  tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
@@ -1,4 +1,6 @@
1
1
  module Turbo::Streams::ActionHelper
2
+ include ActionView::Helpers::TagHelper
3
+
2
4
  # Creates a `turbo-stream` tag according to the passed parameters. Examples:
3
5
  #
4
6
  # turbo_stream_action_tag "remove", target: "message_1"
@@ -9,22 +11,22 @@ module Turbo::Streams::ActionHelper
9
11
  #
10
12
  # turbo_stream_action_tag "replace", targets: "message_1", template: %(<div id="message_1">Hello!</div>)
11
13
  # # => <turbo-stream action="replace" targets="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
12
- def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil)
13
- template = action.to_sym == :remove ? "" : "<template>#{template}</template>"
14
+ def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, **attributes)
15
+ template = action.to_sym == :remove ? "" : tag.template(template.to_s.html_safe)
14
16
 
15
17
  if target = convert_to_turbo_stream_dom_id(target)
16
- %(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
18
+ tag.turbo_stream(template, **attributes, action: action, target: target)
17
19
  elsif targets = convert_to_turbo_stream_dom_id(targets, include_selector: true)
18
- %(<turbo-stream action="#{action}" targets="#{targets}">#{template}</turbo-stream>).html_safe
20
+ tag.turbo_stream(template, **attributes, action: action, targets: targets)
19
21
  else
20
- raise ArgumentError, "target or targets must be supplied"
22
+ tag.turbo_stream(template, **attributes, action: action)
21
23
  end
22
24
  end
23
25
 
24
26
  private
25
27
  def convert_to_turbo_stream_dom_id(target, include_selector: false)
26
28
  if target.respond_to?(:to_key)
27
- [ ("#" if include_selector), ActionView::RecordIdentifier.dom_id(target) ].compact.join
29
+ "#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(target)}"
28
30
  else
29
31
  target
30
32
  end
@@ -44,6 +44,11 @@ module Turbo::StreamsHelper
44
44
  # or a class name):
45
45
  #
46
46
  # <%= turbo_stream_from "room", channel: RoomChannel %>
47
+ #
48
+ # It is also possible to pass additional parameters to the channel by passing them through `data` attributes:
49
+ #
50
+ # <%= turbo_stream_from "room", channel: RoomChannel, data: {room_name: "room #1"} %>
51
+ #
47
52
  def turbo_stream_from(*streamables, **attributes)
48
53
  attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
49
54
  attributes[:"signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables)
@@ -1,10 +1,15 @@
1
1
  import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
2
2
  import { subscribeTo } from "./cable"
3
+ import snakeize from "./snakeize"
3
4
 
4
5
  class TurboCableStreamSourceElement extends HTMLElement {
5
6
  async connectedCallback() {
6
7
  connectStreamSource(this)
7
- this.subscription = await subscribeTo(this.channel, { received: this.dispatchMessageEvent.bind(this) })
8
+ this.subscription = await subscribeTo(this.channel, {
9
+ received: this.dispatchMessageEvent.bind(this),
10
+ connected: this.subscriptionConnected.bind(this),
11
+ disconnected: this.subscriptionDisconnected.bind(this)
12
+ })
8
13
  }
9
14
 
10
15
  disconnectedCallback() {
@@ -17,11 +22,22 @@ class TurboCableStreamSourceElement extends HTMLElement {
17
22
  return this.dispatchEvent(event)
18
23
  }
19
24
 
25
+ subscriptionConnected() {
26
+ this.setAttribute("connected", "")
27
+ }
28
+
29
+ subscriptionDisconnected() {
30
+ this.removeAttribute("connected")
31
+ }
32
+
20
33
  get channel() {
21
34
  const channel = this.getAttribute("channel")
22
35
  const signed_stream_name = this.getAttribute("signed-stream-name")
23
- return { channel, signed_stream_name }
36
+ return { channel, signed_stream_name, ...snakeize({ ...this.dataset }) }
24
37
  }
25
38
  }
26
39
 
27
- customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement)
40
+
41
+ if (customElements.get("turbo-cable-stream-source") === undefined) {
42
+ customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement)
43
+ }
@@ -0,0 +1,50 @@
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
+ if (submitter.hasAttribute("formmethod")) {
39
+ return submitter.formMethod
40
+ } else {
41
+ return null
42
+ }
43
+ } else {
44
+ return null
45
+ }
46
+ }
47
+
48
+ function isBodyInit(body) {
49
+ return body instanceof FormData || body instanceof URLSearchParams
50
+ }
@@ -5,3 +5,7 @@ export { Turbo }
5
5
 
6
6
  import * as cable from "./cable"
7
7
  export { cable }
8
+
9
+ import { encodeMethodIntoRequestBody } from "./fetch_requests"
10
+
11
+ 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
+ };
@@ -27,8 +27,8 @@
27
27
  # (which is derived by default from the plural model name of the model, but can be overwritten).
28
28
  #
29
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
31
- #
30
+ # you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
31
+ #
32
32
  # class Message < ApplicationRecord
33
33
  # belongs_to :user
34
34
  #
@@ -39,7 +39,21 @@
39
39
  # broadcast_update_to(user, :messages, target: "message-count", html: "<p> #{user.messages.count} </p>")
40
40
  # end
41
41
  # end
42
- #
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
+ #
43
57
  # There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
44
58
  # <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
45
59
  # within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
@@ -66,16 +80,22 @@ module Turbo::Broadcastable
66
80
  # belongs_to :board
67
81
  # broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages"
68
82
  # end
69
- def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default)
70
- after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target }
71
- after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream) }
72
- after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) }
83
+ #
84
+ # class Message < ApplicationRecord
85
+ # belongs_to :board
86
+ # broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
87
+ # end
88
+ def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default, **rendering)
89
+ after_create_commit -> { broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering) }
90
+ after_update_commit -> { broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
91
+ after_destroy_commit -> { broadcast_remove_to(stream.try(:call, self) || send(stream)) }
73
92
  end
74
93
 
75
- # Same as <tt>#broadcasts_to</tt>, but the designated stream is automatically set to the current model.
76
- def broadcasts(inserts_by: :append, target: broadcast_target_default)
77
- after_create_commit -> { broadcast_action_later action: inserts_by, target: target.try(:call, self) || target }
78
- after_update_commit -> { broadcast_replace_later }
94
+ # Same as <tt>#broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
95
+ # the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
96
+ def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcast_target_default, **rendering)
97
+ after_create_commit -> { broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering) }
98
+ after_update_commit -> { broadcast_replace_later(**rendering) }
79
99
  after_destroy_commit -> { broadcast_remove }
80
100
  end
81
101
 
@@ -329,8 +349,13 @@ module Turbo::Broadcastable
329
349
  # Add the current instance into the locals with the element name (which is the un-namespaced name)
330
350
  # as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
331
351
  o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
332
- # if the html option is passed in it will skip setting a partial from #to_partial_path
333
- unless o.include?(:html)
352
+
353
+ if o[:html] || o[:partial]
354
+ return o
355
+ elsif o[:template]
356
+ o[:layout] = false
357
+ else
358
+ # if none of these options are passed in, it will set a partial from #to_partial_path
334
359
  o[:partial] ||= to_partial_path
335
360
  end
336
361
  end
@@ -50,7 +50,7 @@ class Turbo::Streams::TagBuilder
50
50
  action_all :remove, targets, allow_inferred_rendering: false
51
51
  end
52
52
 
53
- # Replace the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
53
+ # Replace the <tt>target</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
54
54
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
55
55
  #
56
56
  # <%= turbo_stream.replace "clearance_5", "<div id='clearance_5'>Replace the dom target identified by clearance_5</div>" %>
@@ -63,7 +63,7 @@ class Turbo::Streams::TagBuilder
63
63
  action :replace, target, content, **rendering, &block
64
64
  end
65
65
 
66
- # Replace the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
66
+ # Replace the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
67
67
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
68
68
  #
69
69
  # <%= turbo_stream.replace_all ".clearance_item", "<div class='clearance_item'>Replace the dom target identified by the class clearance_item</div>" %>
@@ -128,7 +128,7 @@ class Turbo::Streams::TagBuilder
128
128
  action_all :after, targets, content, **rendering, &block
129
129
  end
130
130
 
131
- # Update the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
131
+ # Update the <tt>target</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
132
132
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
133
133
  #
134
134
  # <%= turbo_stream.update "clearance_5", "Update the content of the dom target identified by clearance_5" %>
@@ -141,7 +141,7 @@ class Turbo::Streams::TagBuilder
141
141
  action :update, target, content, **rendering, &block
142
142
  end
143
143
 
144
- # Update the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
144
+ # Update the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
145
145
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the targets as a record. Examples:
146
146
  #
147
147
  # <%= turbo_stream.update_all "clearance_item", "Update the content of the dom target identified by the class clearance_item" %>
@@ -227,6 +227,8 @@ class Turbo::Streams::TagBuilder
227
227
  private
228
228
  def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
229
229
  case
230
+ when content.respond_to?(:render_in)
231
+ content.render_in(@view_context, &block)
230
232
  when content
231
233
  allow_inferred_rendering ? (render_record(content) || content) : content
232
234
  when block_given?
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <%= yield :head %>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
@@ -3,7 +3,9 @@ def run_turbo_install_template(path)
3
3
  end
4
4
 
5
5
  def redis_installed?
6
- system('which redis-server > /dev/null')
6
+ Gem.win_platform? ?
7
+ system('where redis-server > NUL 2>&1') :
8
+ system('which redis-server > /dev/null')
7
9
  end
8
10
 
9
11
  def switch_on_redis_if_available
data/lib/turbo/engine.rb CHANGED
@@ -16,8 +16,8 @@ module Turbo
16
16
  #{root}/app/jobs
17
17
  )
18
18
 
19
- initializer "turbo.no_action_cable" do
20
- Rails.autoloaders.once.do_not_eager_load(Dir["#{root}/app/channels/turbo/*_channel.rb"]) unless defined?(ActionCable)
19
+ initializer "turbo.no_action_cable", before: :set_eager_load_paths do
20
+ config.eager_load_paths.delete("#{root}/app/channels") unless defined?(ActionCable)
21
21
  end
22
22
 
23
23
  # If you don't want to precompile Turbo's assets (eg. because you're using webpack),
@@ -61,8 +61,10 @@ module Turbo
61
61
  end
62
62
 
63
63
  initializer "turbo.signed_stream_verifier_key" do
64
- Turbo.signed_stream_verifier_key = config.turbo.signed_stream_verifier_key ||
65
- Rails.application.key_generator.generate_key("turbo/signed_stream_verifier_key")
64
+ config.after_initialize do
65
+ Turbo.signed_stream_verifier_key = config.turbo.signed_stream_verifier_key ||
66
+ Rails.application.key_generator.generate_key("turbo/signed_stream_verifier_key")
67
+ end
66
68
  end
67
69
 
68
70
  initializer "turbo.test_assertions" do
@@ -7,15 +7,21 @@ module Turbo
7
7
  delegate :dom_id, :dom_class, to: ActionView::RecordIdentifier
8
8
  end
9
9
 
10
- def assert_turbo_stream(action:, target: nil, status: :ok, &block)
10
+ def assert_turbo_stream(action:, target: nil, targets: nil, status: :ok, &block)
11
11
  assert_response status
12
12
  assert_equal Mime[:turbo_stream], response.media_type
13
- assert_select %(turbo-stream[action="#{action}"][target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]), count: 1, &block
13
+ selector = %(turbo-stream[action="#{action}"])
14
+ selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
15
+ selector << %([targets="#{targets}"]) if targets
16
+ assert_select selector, count: 1, &block
14
17
  end
15
18
 
16
- def assert_no_turbo_stream(action:, target: nil)
19
+ def assert_no_turbo_stream(action:, target: nil, targets: nil)
17
20
  assert_equal Mime[:turbo_stream], response.media_type
18
- assert_select %(turbo-stream[action="#{action}"][target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]), count: 0
21
+ selector = %(turbo-stream[action="#{action}"])
22
+ selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
23
+ selector << %([targets="#{targets}"]) if targets
24
+ assert_select selector, count: 0
19
25
  end
20
26
  end
21
27
  end
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "1.0.1"
2
+ VERSION = "1.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,8 +10,22 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-01-16 00:00:00.000000000 Z
13
+ date: 2023-03-01 00:00:00.000000000 Z
14
14
  dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activejob
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 6.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 6.0.0
15
29
  - !ruby/object:Gem::Dependency
16
30
  name: actionpack
17
31
  requirement: !ruby/object:Gem::Requirement
@@ -66,11 +80,14 @@ files:
66
80
  - app/helpers/turbo/streams_helper.rb
67
81
  - app/javascript/turbo/cable.js
68
82
  - app/javascript/turbo/cable_stream_source_element.js
83
+ - app/javascript/turbo/fetch_requests.js
69
84
  - app/javascript/turbo/index.js
85
+ - app/javascript/turbo/snakeize.js
70
86
  - app/jobs/turbo/streams/action_broadcast_job.rb
71
87
  - app/jobs/turbo/streams/broadcast_job.rb
72
88
  - app/models/concerns/turbo/broadcastable.rb
73
89
  - app/models/turbo/streams/tag_builder.rb
90
+ - app/views/layouts/turbo_rails/frame.html.erb
74
91
  - config/routes.rb
75
92
  - lib/install/turbo_needs_redis.rb
76
93
  - lib/install/turbo_with_importmap.rb
@@ -99,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
116
  - !ruby/object:Gem::Version
100
117
  version: '0'
101
118
  requirements: []
102
- rubygems_version: 3.2.32
119
+ rubygems_version: 3.4.6
103
120
  signing_key:
104
121
  specification_version: 4
105
122
  summary: The speed of a single-page web application without having to write any JavaScript.