turbo-rails 1.1.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/Rakefile +15 -2
- data/app/assets/javascripts/turbo.js +1214 -561
- data/app/assets/javascripts/turbo.min.js +6 -6
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +1 -1
- data/app/controllers/turbo/frames/frame_request.rb +15 -7
- data/app/controllers/turbo/native/navigation.rb +3 -1
- data/app/helpers/turbo/drive_helper.rb +16 -3
- data/app/helpers/turbo/streams/action_helper.rb +8 -6
- data/app/javascript/turbo/cable_stream_source_element.js +17 -2
- data/app/javascript/turbo/fetch_requests.js +50 -0
- data/app/javascript/turbo/index.js +3 -2
- data/app/models/concerns/turbo/broadcastable.rb +29 -10
- data/app/models/turbo/streams/tag_builder.rb +2 -0
- data/app/views/layouts/turbo_rails/frame.html.erb +8 -0
- data/lib/turbo/version.rb +1 -1
- metadata +5 -4
- data/app/javascript/turbo/form_submissions.js +0 -7
@@ -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.
|
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
|
-
#
|
9
|
-
#
|
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 -> {
|
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]
|
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
|
-
#
|
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,
|
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
|
@@ -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 ? "" :
|
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
|
-
|
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
|
-
|
20
|
+
tag.turbo_stream(template, **attributes, action: action, targets: targets)
|
19
21
|
else
|
20
|
-
|
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
|
-
|
29
|
+
"#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(target)}"
|
28
30
|
else
|
29
31
|
target
|
30
32
|
end
|
@@ -5,7 +5,11 @@ import snakeize from "./snakeize"
|
|
5
5
|
class TurboCableStreamSourceElement extends HTMLElement {
|
6
6
|
async connectedCallback() {
|
7
7
|
connectStreamSource(this)
|
8
|
-
this.subscription = await subscribeTo(this.channel, {
|
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
|
+
})
|
9
13
|
}
|
10
14
|
|
11
15
|
disconnectedCallback() {
|
@@ -18,6 +22,14 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
18
22
|
return this.dispatchEvent(event)
|
19
23
|
}
|
20
24
|
|
25
|
+
subscriptionConnected() {
|
26
|
+
this.setAttribute("connected", "")
|
27
|
+
}
|
28
|
+
|
29
|
+
subscriptionDisconnected() {
|
30
|
+
this.removeAttribute("connected")
|
31
|
+
}
|
32
|
+
|
21
33
|
get channel() {
|
22
34
|
const channel = this.getAttribute("channel")
|
23
35
|
const signed_stream_name = this.getAttribute("signed-stream-name")
|
@@ -25,4 +37,7 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
25
37
|
}
|
26
38
|
}
|
27
39
|
|
28
|
-
|
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
|
+
}
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import "./cable_stream_source_element"
|
2
|
-
import { overrideMethodWithFormmethod } from "./form_submissions"
|
3
2
|
|
4
3
|
import * as Turbo from "@hotwired/turbo"
|
5
4
|
export { Turbo }
|
@@ -7,4 +6,6 @@ export { Turbo }
|
|
7
6
|
import * as cable from "./cable"
|
8
7
|
export { cable }
|
9
8
|
|
10
|
-
|
9
|
+
import { encodeMethodIntoRequestBody } from "./fetch_requests"
|
10
|
+
|
11
|
+
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)
|
@@ -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
|
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
|
@@ -72,16 +86,16 @@ module Turbo::Broadcastable
|
|
72
86
|
# broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
|
73
87
|
# end
|
74
88
|
def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default, **rendering)
|
75
|
-
after_create_commit -> { broadcast_action_later_to
|
76
|
-
after_update_commit -> { broadcast_replace_later_to
|
77
|
-
after_destroy_commit -> { broadcast_remove_to
|
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)) }
|
78
92
|
end
|
79
93
|
|
80
94
|
# Same as <tt>#broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
|
81
95
|
# the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
|
82
96
|
def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcast_target_default, **rendering)
|
83
|
-
after_create_commit -> { broadcast_action_later_to
|
84
|
-
after_update_commit -> { broadcast_replace_later
|
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) }
|
85
99
|
after_destroy_commit -> { broadcast_remove }
|
86
100
|
end
|
87
101
|
|
@@ -335,8 +349,13 @@ module Turbo::Broadcastable
|
|
335
349
|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
336
350
|
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
|
337
351
|
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
|
338
|
-
|
339
|
-
|
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
|
340
359
|
o[:partial] ||= to_partial_path
|
341
360
|
end
|
342
361
|
end
|
@@ -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?
|
data/lib/turbo/version.rb
CHANGED
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.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Stephenson
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2023-03-01 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activejob
|
@@ -80,13 +80,14 @@ files:
|
|
80
80
|
- app/helpers/turbo/streams_helper.rb
|
81
81
|
- app/javascript/turbo/cable.js
|
82
82
|
- app/javascript/turbo/cable_stream_source_element.js
|
83
|
-
- app/javascript/turbo/
|
83
|
+
- app/javascript/turbo/fetch_requests.js
|
84
84
|
- app/javascript/turbo/index.js
|
85
85
|
- app/javascript/turbo/snakeize.js
|
86
86
|
- app/jobs/turbo/streams/action_broadcast_job.rb
|
87
87
|
- app/jobs/turbo/streams/broadcast_job.rb
|
88
88
|
- app/models/concerns/turbo/broadcastable.rb
|
89
89
|
- app/models/turbo/streams/tag_builder.rb
|
90
|
+
- app/views/layouts/turbo_rails/frame.html.erb
|
90
91
|
- config/routes.rb
|
91
92
|
- lib/install/turbo_needs_redis.rb
|
92
93
|
- lib/install/turbo_with_importmap.rb
|
@@ -115,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
116
|
- !ruby/object:Gem::Version
|
116
117
|
version: '0'
|
117
118
|
requirements: []
|
118
|
-
rubygems_version: 3.
|
119
|
+
rubygems_version: 3.4.6
|
119
120
|
signing_key:
|
120
121
|
specification_version: 4
|
121
122
|
summary: The speed of a single-page web application without having to write any JavaScript.
|