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.
@@ -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
@@ -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
@@ -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, { 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
+ })
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
- 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
+ }
@@ -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
- addEventListener("turbo:submit-start", overrideMethodWithFormmethod)
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: 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
@@ -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 stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering }
76
- after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream), **rendering }
77
- after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) }
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 stream, action: inserts_by, target: target.try(:call, self) || target, **rendering }
84
- after_update_commit -> { broadcast_replace_later **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) }
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
- # if the html option is passed in it will skip setting a partial from #to_partial_path
339
- 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
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?
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <%= yield :head %>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "1.1.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.1.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: 2022-05-23 00:00:00.000000000 Z
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/form_submissions.js
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.2.33
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.
@@ -1,7 +0,0 @@
1
- export function overrideMethodWithFormmethod({ detail: { formSubmission: { fetchRequest, submitter } } }) {
2
- const formMethod = submitter?.formMethod
3
-
4
- if (formMethod && fetchRequest.body.has("_method")) {
5
- fetchRequest.body.set("_method", formMethod)
6
- }
7
- }