turbo-rails 1.5.0 → 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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