turbo-rails 1.5.0 → 2.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +67 -16
- data/app/assets/javascripts/turbo.js +1974 -785
- data/app/assets/javascripts/turbo.min.js +9 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +33 -7
- data/app/channels/turbo/streams_channel.rb +15 -15
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/controllers/turbo/frames/frame_request.rb +2 -2
- data/app/controllers/turbo/native/navigation.rb +6 -3
- data/app/helpers/turbo/drive_helper.rb +72 -14
- data/app/helpers/turbo/frames_helper.rb +8 -8
- data/app/helpers/turbo/streams/action_helper.rb +12 -4
- data/app/helpers/turbo/streams_helper.rb +5 -0
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +184 -35
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +20 -0
- data/app/models/turbo/thread_debouncer.rb +28 -0
- data/config/routes.rb +3 -4
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/broadcastable/test_helper.rb +5 -5
- data/lib/turbo/engine.rb +34 -8
- data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
- data/lib/turbo/test_assertions.rb +2 -2
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +9 -5
- data/lib/install/turbo_needs_redis.rb +0 -20
@@ -1,4 +1,4 @@
|
|
1
|
-
# Provides the broadcast actions in synchronous and
|
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
|
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,22 @@ module Turbo::Streams::Broadcasts
|
|
63
68
|
broadcast_action_later_to(*streamables, action: :prepend, **opts)
|
64
69
|
end
|
65
70
|
|
66
|
-
def
|
67
|
-
|
68
|
-
stream_name_from(streamables),
|
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)
|
78
|
+
streamables.flatten!
|
79
|
+
streamables.compact_blank!
|
80
|
+
|
81
|
+
if streamables.present?
|
82
|
+
target = convert_to_turbo_stream_dom_id(target)
|
83
|
+
targets = convert_to_turbo_stream_dom_id(targets, include_selector: true)
|
84
|
+
Turbo::Streams::ActionBroadcastJob.perform_later \
|
85
|
+
stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
|
86
|
+
end
|
69
87
|
end
|
70
88
|
|
71
89
|
def broadcast_render_to(*streamables, **rendering)
|
@@ -77,9 +95,17 @@ module Turbo::Streams::Broadcasts
|
|
77
95
|
end
|
78
96
|
|
79
97
|
def broadcast_stream_to(*streamables, content:)
|
80
|
-
|
98
|
+
streamables.flatten!
|
99
|
+
streamables.compact_blank!
|
100
|
+
|
101
|
+
if streamables.present?
|
102
|
+
ActionCable.server.broadcast stream_name_from(streamables), content
|
103
|
+
end
|
81
104
|
end
|
82
105
|
|
106
|
+
def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
|
107
|
+
Turbo::ThreadDebouncer.for("turbo-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
|
108
|
+
end
|
83
109
|
|
84
110
|
private
|
85
111
|
def render_format(format, **rendering)
|
@@ -9,27 +9,27 @@
|
|
9
9
|
# helper modules like <tt>Turbo::Streams::StreamName</tt>:
|
10
10
|
#
|
11
11
|
# class CustomChannel < ActionCable::Channel::Base
|
12
|
-
#
|
13
|
-
#
|
12
|
+
# extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
|
13
|
+
# include Turbo::Streams::StreamName::ClassMethods
|
14
14
|
#
|
15
|
-
#
|
16
|
-
#
|
15
|
+
# def subscribed
|
16
|
+
# if (stream_name = verified_stream_name_from_params).present? &&
|
17
17
|
# subscription_allowed?
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
18
|
+
# stream_from stream_name
|
19
|
+
# else
|
20
|
+
# reject
|
21
|
+
# end
|
22
|
+
# end
|
23
23
|
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
24
|
+
# def subscription_allowed?
|
25
|
+
# # ...
|
26
|
+
# end
|
27
27
|
# end
|
28
28
|
#
|
29
|
-
#
|
30
|
-
#
|
29
|
+
# This channel can be connected to a web page using <tt>:channel</tt> option in
|
30
|
+
# <tt>turbo_stream_from</tt> helper:
|
31
31
|
#
|
32
|
-
#
|
32
|
+
# <%= turbo_stream_from 'room', channel: CustomChannel %>
|
33
33
|
#
|
34
34
|
class Turbo::StreamsChannel < ActionCable::Channel::Base
|
35
35
|
extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
|
@@ -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
|
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.
|
@@ -24,7 +24,7 @@ module Turbo::Frames::FrameRequest
|
|
24
24
|
layout -> { "turbo_rails/frame" if turbo_frame_request? }
|
25
25
|
etag { :frame if turbo_frame_request? }
|
26
26
|
|
27
|
-
helper_method :turbo_frame_request_id
|
27
|
+
helper_method :turbo_frame_request?, :turbo_frame_request_id
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
@@ -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,
|
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,
|
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,
|
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
|
-
#
|
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
|
-
#
|
28
|
-
#
|
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="
|
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.
|
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
|
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,10 +34,19 @@ 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
|
-
|
41
|
-
|
47
|
+
target_array = Array.wrap(target)
|
48
|
+
if target_array.any? { |value| value.respond_to?(:to_key) }
|
49
|
+
"#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(*target_array)}"
|
42
50
|
else
|
43
51
|
target
|
44
52
|
end
|
@@ -49,7 +49,12 @@ module Turbo::StreamsHelper
|
|
49
49
|
#
|
50
50
|
# <%= turbo_stream_from "room", channel: RoomChannel, data: {room_name: "room #1"} %>
|
51
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
|
52
56
|
def turbo_stream_from(*streamables, **attributes)
|
57
|
+
raise ArgumentError, "streamables can't be blank" unless streamables.any?(&:present?)
|
53
58
|
attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
|
54
59
|
attributes[:"signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables)
|
55
60
|
|
@@ -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
|