turbo-rails 1.5.0 → 2.0.2

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.
@@ -1,4 +1,4 @@
1
- # Provides the broadcast actions in synchronous and asynchrous form for the <tt>Turbo::StreamsChannel</tt>.
1
+ # Provides the broadcast actions in synchronous and asynchronous form for the <tt>Turbo::StreamsChannel</tt>.
2
2
  # See <tt>Turbo::Broadcastable</tt> for the user-facing API that invokes these methods with most of the paperwork filled out already.
3
3
  #
4
4
  # Can be used directly using something like <tt>Turbo::StreamsChannel.broadcast_remove_to :entries, target: 1</tt>.
@@ -33,9 +33,14 @@ module Turbo::Streams::Broadcasts
33
33
  broadcast_action_to(*streamables, action: :prepend, **opts)
34
34
  end
35
35
 
36
- def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
36
+ def broadcast_refresh_to(*streamables, **opts)
37
+ broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag)
38
+ end
39
+
40
+ def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
37
41
  broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
38
- rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil)
42
+ rendering.delete(:content) || rendering.delete(:html) || (rendering[:render] != false && rendering.any? ? render_format(:html, **rendering) : nil),
43
+ **attributes
39
44
  ))
40
45
  end
41
46
 
@@ -63,9 +68,15 @@ module Turbo::Streams::Broadcasts
63
68
  broadcast_action_later_to(*streamables, action: :prepend, **opts)
64
69
  end
65
70
 
66
- def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering)
71
+ def broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **opts)
72
+ 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)
74
+ end
75
+ end
76
+
77
+ def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
67
78
  Turbo::Streams::ActionBroadcastJob.perform_later \
68
- stream_name_from(streamables), action: action, target: target, targets: targets, **rendering
79
+ stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
69
80
  end
70
81
 
71
82
  def broadcast_render_to(*streamables, **rendering)
@@ -80,6 +91,9 @@ module Turbo::Streams::Broadcasts
80
91
  ActionCable.server.broadcast stream_name_from(streamables), content
81
92
  end
82
93
 
94
+ def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
95
+ Turbo::ThreadDebouncer.for("turbo-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
96
+ end
83
97
 
84
98
  private
85
99
  def render_format(format, **rendering)
@@ -0,0 +1,12 @@
1
+ module Turbo::RequestIdTracking
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ around_action :turbo_tracking_request_id
6
+ end
7
+
8
+ private
9
+ def turbo_tracking_request_id(&block)
10
+ Turbo.with_request_id(request.headers["X-Turbo-Request-Id"], &block)
11
+ end
12
+ end
@@ -5,7 +5,7 @@
5
5
  # When that header is detected by the controller, we substitute our own minimal layout in place of the
6
6
  # application-supplied layout (since we're only working on an in-page frame, thus can skip the weight of the layout). We
7
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>.
8
+ # <tt>head</tt>.
9
9
  #
10
10
  # Accordingly, we ensure that the etag for the page is changed, such that a cache for a minimal-layout request isn't
11
11
  # served on a normal request and vice versa.
@@ -33,6 +33,6 @@ module Turbo::Frames::FrameRequest
33
33
  end
34
34
 
35
35
  def turbo_frame_request_id
36
- request.headers["Turbo-Frame"]
36
+ request&.headers["Turbo-Frame"]
37
37
  end
38
38
  end
@@ -15,29 +15,32 @@ module Turbo::Native::Navigation
15
15
  request.user_agent.to_s.match?(/Turbo Native/)
16
16
  end
17
17
 
18
- # Tell the Turbo Native app to dismiss a modal (if presented) or pop a screen off of the navigation stack.
18
+ # Tell the Turbo Native app to dismiss a modal (if presented) or pop a screen off of the navigation stack. Otherwise redirect to the given URL if Turbo Native is not present.
19
19
  def recede_or_redirect_to(url, **options)
20
20
  turbo_native_action_or_redirect url, :recede, :to, options
21
21
  end
22
22
 
23
- # Tell the Turbo Native app to ignore this navigation.
23
+ # Tell the Turbo Native app to ignore this navigation, otherwise redirect to the given URL if Turbo Native is not present.
24
24
  def resume_or_redirect_to(url, **options)
25
25
  turbo_native_action_or_redirect url, :resume, :to, options
26
26
  end
27
27
 
28
- # Tell the Turbo Native app to refresh the current screen.
28
+ # Tell the Turbo Native app to refresh the current screen, otherwise redirect to the given URL if Turbo Native is not present.
29
29
  def refresh_or_redirect_to(url, **options)
30
30
  turbo_native_action_or_redirect url, :refresh, :to, options
31
31
  end
32
32
 
33
+ # Same as <tt>recede_or_redirect_to</tt> but redirects to the previous page or provided fallback location if the Turbo Native app is not present.
33
34
  def recede_or_redirect_back_or_to(url, **options)
34
35
  turbo_native_action_or_redirect url, :recede, :back, options
35
36
  end
36
37
 
38
+ # Same as <tt>resume_or_redirect_to</tt> but redirects to the previous page or provided fallback location if the Turbo Native app is not present.
37
39
  def resume_or_redirect_back_or_to(url, **options)
38
40
  turbo_native_action_or_redirect url, :resume, :back, options
39
41
  end
40
42
 
43
+ # Same as <tt>refresh_or_redirect_to</tt> but redirects to the previous page or provided fallback location if the Turbo Native app is not present.
41
44
  def refresh_or_redirect_back_or_to(url, **options)
42
45
  turbo_native_action_or_redirect url, :refresh, :back, options
43
46
  end
@@ -1,29 +1,87 @@
1
+ # Helpers to configure Turbo Drive via meta directives. They come in two
2
+ # variants:
3
+ #
4
+ # The recommended option is to include +yield :head+ in the +<head>+ section
5
+ # of the layout. Then you can use the helpers in any view.
6
+ #
7
+ # ==== Example
8
+ #
9
+ # # app/views/application.html.erb
10
+ # <html><head><%= yield :head %></head><body><%= yield %></html>
11
+ #
12
+ # # app/views/trays/index.html.erb
13
+ # <% turbo_exempts_page_from_cache %>
14
+ # <p>Page that shouldn't be cached by Turbo</p>
15
+ #
16
+ # Alternatively, you can use the +_tag+ variant of the helpers to only get the
17
+ # HTML for the meta directive.
1
18
  module Turbo::DriveHelper
2
- # Note: These helpers require a +yield :head+ provision in the layout.
3
- #
4
- # ==== Example
5
- #
6
- # # app/views/application.html.erb
7
- # <html><head><%= yield :head %></head><body><%= yield %></html>
8
- #
9
- # # app/views/trays/index.html.erb
10
- # <% turbo_exempts_page_from_cache %>
11
- # <p>Page that shouldn't be cached by Turbo</p>
12
-
13
19
  # Pages that are more likely than not to be a cache miss can skip turbo cache to avoid visual jitter.
14
20
  # Cannot be used along with +turbo_exempts_page_from_preview+.
15
21
  def turbo_exempts_page_from_cache
16
- provide :head, tag.meta(name: "turbo-cache-control", content: "no-cache")
22
+ provide :head, turbo_exempts_page_from_cache_tag
23
+ end
24
+
25
+ # See +turbo_exempts_page_from_cache+.
26
+ def turbo_exempts_page_from_cache_tag
27
+ tag.meta(name: "turbo-cache-control", content: "no-cache")
17
28
  end
18
29
 
19
30
  # Specify that a cached version of the page should not be shown as a preview during an application visit.
20
31
  # Cannot be used along with +turbo_exempts_page_from_cache+.
21
32
  def turbo_exempts_page_from_preview
22
- provide :head, tag.meta(name: "turbo-cache-control", content: "no-preview")
33
+ provide :head, turbo_exempts_page_from_preview_tag
34
+ end
35
+
36
+ # See +turbo_exempts_page_from_preview+.
37
+ def turbo_exempts_page_from_preview_tag
38
+ tag.meta(name: "turbo-cache-control", content: "no-preview")
23
39
  end
24
40
 
25
41
  # Force the page, when loaded by Turbo, to be cause a full page reload.
26
42
  def turbo_page_requires_reload
27
- provide :head, tag.meta(name: "turbo-visit-control", content: "reload")
43
+ provide :head, turbo_page_requires_reload_tag
44
+ end
45
+
46
+ # See +turbo_page_requires_reload+.
47
+ def turbo_page_requires_reload_tag
48
+ tag.meta(name: "turbo-visit-control", content: "reload")
49
+ end
50
+
51
+ # Configure how to handle page refreshes. A page refresh happens when
52
+ # Turbo loads the current page again with a *replace* visit:
53
+ #
54
+ # === Parameters:
55
+ #
56
+ # * <tt>method</tt> - Method to update the +<body>+ of the page
57
+ # during a page refresh. It can be one of:
58
+ # * +replace:+: Replaces the existing +<body>+ with the new one. This is the
59
+ # default behavior.
60
+ # * +morph:+: Morphs the existing +<body>+ into the new one.
61
+ #
62
+ # * <tt>scroll</tt> - Controls the scroll behavior when a page refresh happens. It
63
+ # can be one of:
64
+ # * +reset:+: Resets scroll to the top, left corner. This is the default.
65
+ # * +preserve:+: Keeps the scroll.
66
+ #
67
+ # === Example Usage:
68
+ #
69
+ # turbo_refreshes_with(method: :morph, scroll: :preserve)
70
+ def turbo_refreshes_with(method: :replace, scroll: :reset)
71
+ provide :head, turbo_refresh_method_tag(method)
72
+ provide :head, turbo_refresh_scroll_tag(scroll)
73
+ end
74
+
75
+ # Configure method to perform page refreshes. See +turbo_refreshes_with+.
76
+ def turbo_refresh_method_tag(method = :replace)
77
+ raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ])
78
+ tag.meta(name: "turbo-refresh-method", content: method)
79
+ end
80
+
81
+ # Configure scroll strategy for page refreshes. See +turbo_refreshes_with+.
82
+ def turbo_refresh_scroll_tag(scroll = :reset)
83
+ raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ])
84
+ tag.meta(name: "turbo-refresh-scroll", content: scroll)
28
85
  end
29
86
  end
87
+
@@ -2,7 +2,7 @@ module Turbo::FramesHelper
2
2
  # Returns a frame tag that can either be used simply to encapsulate frame content or as a lazy-loading container that starts empty but
3
3
  # fetches the URL supplied in the +src+ attribute.
4
4
  #
5
- # === Examples
5
+ # ==== Examples
6
6
  #
7
7
  # <%= turbo_frame_tag "tray", src: tray_path(tray) %>
8
8
  # # => <turbo-frame id="tray" src="http://example.com/trays/1"></turbo-frame>
@@ -24,19 +24,19 @@ module Turbo::FramesHelper
24
24
  # <% end %>
25
25
  # # => <turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
26
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:
27
+ # <%= turbo_frame_tag [user_id, "tray"], src: tray_path(tray) %>
28
+ # # => <turbo-frame id="1_tray" src="http://example.com/trays/1"></turbo-frame>
29
+ #
30
+ # The +turbo_frame_tag+ helper will convert the arguments it receives to their
31
+ # +dom_id+ if applicable to easily generate unique ids for Turbo Frames:
29
32
  #
30
33
  # <%= turbo_frame_tag(Article.find(1)) %>
31
34
  # # => <turbo-frame id="article_1"></turbo-frame>
32
35
  #
33
36
  # <%= 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>
37
+ # # => <turbo-frame id="comments_article_1"></turbo-frame>
38
38
  def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
39
- id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.first
39
+ id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
40
40
  src = url_for(src) if src.present?
41
41
 
42
42
  tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
@@ -22,9 +22,8 @@ module Turbo::Streams::ActionHelper
22
22
  # message = Message.find(1)
23
23
  # turbo_stream_action_tag "remove", target: [message, :special]
24
24
  # # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
25
- #
26
25
  def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, **attributes)
27
- template = action.to_sym == :remove ? "" : tag.template(template.to_s.html_safe)
26
+ template = action.to_sym.in?(%i[ remove refresh ]) ? "" : tag.template(template.to_s.html_safe)
28
27
 
29
28
  if target = convert_to_turbo_stream_dom_id(target)
30
29
  tag.turbo_stream(template, **attributes, action: action, target: target)
@@ -35,6 +34,14 @@ module Turbo::Streams::ActionHelper
35
34
  end
36
35
  end
37
36
 
37
+ # Creates a `turbo-stream` tag with an `action="refresh"` attribute. Example:
38
+ #
39
+ # turbo_stream_refresh_tag
40
+ # # => <turbo-stream action="refresh"></turbo-stream>
41
+ def turbo_stream_refresh_tag(request_id: Turbo.current_request_id, **attributes)
42
+ turbo_stream_action_tag(:refresh, **{ "request-id": request_id }.compact, **attributes)
43
+ end
44
+
38
45
  private
39
46
  def convert_to_turbo_stream_dom_id(target, include_selector: false)
40
47
  if Array(target).any? { |value| value.respond_to?(:to_key) }
@@ -48,7 +48,6 @@ 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
51
  def turbo_stream_from(*streamables, **attributes)
53
52
  attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
54
53
  attributes[:"signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables)
@@ -8,4 +8,6 @@ export { cable }
8
8
 
9
9
  import { encodeMethodIntoRequestBody } from "./fetch_requests"
10
10
 
11
+ window.Turbo = Turbo
12
+
11
13
  addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)
@@ -2,7 +2,7 @@
2
2
  class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base
3
3
  discard_on ActiveJob::DeserializationError
4
4
 
5
- def perform(stream, action:, target:, **rendering)
6
- Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering
5
+ def perform(stream, action:, target:, attributes: {}, **rendering)
6
+ Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, attributes: attributes, **rendering
7
7
  end
8
8
  end
@@ -2,7 +2,7 @@
2
2
  # turbo stream templates.
3
3
  class Turbo::Streams::BroadcastJob < ActiveJob::Base
4
4
  discard_on ActiveJob::DeserializationError
5
-
5
+
6
6
  def perform(stream, **rendering)
7
7
  Turbo::StreamsChannel.broadcast_render_to stream, **rendering
8
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