turbo-rails 0.9.1 → 1.1.0

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.
@@ -9,7 +9,7 @@
9
9
  # helper modules like <tt>Turbo::Streams::StreamName</tt>:
10
10
  #
11
11
  # class CustomChannel < ActionCable::Channel::Base
12
- # extend Turbo::Stream::Broadcasts, Turbo::Streams::StreamName
12
+ # extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
13
13
  # include Turbo::Streams::StreamName::ClassMethods
14
14
  #
15
15
  # def subscribed
@@ -23,8 +23,20 @@ module Turbo::FramesHelper
23
23
  # <div>My tray frame!</div>
24
24
  # <% end %>
25
25
  # # => <turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
26
- def turbo_frame_tag(id, src: nil, target: nil, **attributes, &block)
27
- id = id.respond_to?(:to_key) ? dom_id(id) : id
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:
29
+ #
30
+ # <%= turbo_frame_tag(Article.find(1)) %>
31
+ # # => <turbo-frame id="article_1"></turbo-frame>
32
+ #
33
+ # <%= 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>
38
+ def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
39
+ id = ids.map { |id| id.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(id) : id }.join("_")
28
40
  src = url_for(src) if src.present?
29
41
 
30
42
  tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
@@ -44,6 +44,11 @@ module Turbo::StreamsHelper
44
44
  # or a class name):
45
45
  #
46
46
  # <%= turbo_stream_from "room", channel: RoomChannel %>
47
+ #
48
+ # It is also possible to pass additional parameters to the channel by passing them through `data` attributes:
49
+ #
50
+ # <%= turbo_stream_from "room", channel: RoomChannel, data: {room_name: "room #1"} %>
51
+ #
47
52
  def turbo_stream_from(*streamables, **attributes)
48
53
  attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
49
54
  attributes[:"signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables)
@@ -1,5 +1,6 @@
1
1
  import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
2
2
  import { subscribeTo } from "./cable"
3
+ import snakeize from "./snakeize"
3
4
 
4
5
  class TurboCableStreamSourceElement extends HTMLElement {
5
6
  async connectedCallback() {
@@ -20,7 +21,7 @@ class TurboCableStreamSourceElement extends HTMLElement {
20
21
  get channel() {
21
22
  const channel = this.getAttribute("channel")
22
23
  const signed_stream_name = this.getAttribute("signed-stream-name")
23
- return { channel, signed_stream_name }
24
+ return { channel, signed_stream_name, ...snakeize({ ...this.dataset }) }
24
25
  }
25
26
  }
26
27
 
@@ -0,0 +1,7 @@
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
+ }
@@ -1,7 +1,10 @@
1
1
  import "./cable_stream_source_element"
2
+ import { overrideMethodWithFormmethod } from "./form_submissions"
2
3
 
3
4
  import * as Turbo from "@hotwired/turbo"
4
5
  export { Turbo }
5
6
 
6
7
  import * as cable from "./cable"
7
8
  export { cable }
9
+
10
+ addEventListener("turbo:submit-start", overrideMethodWithFormmethod)
@@ -0,0 +1,31 @@
1
+ // Based on https://github.com/nathan7/snakeize
2
+ //
3
+ // This software is released under the MIT license:
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ // this software and associated documentation files (the "Software"), to deal in
6
+ // the Software without restriction, including without limitation the rights to
7
+ // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ // the Software, and to permit persons to whom the Software is furnished to do so,
9
+ // subject to the following conditions:
10
+
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ export default function walk (obj) {
21
+ if (!obj || typeof obj !== 'object') return obj;
22
+ if (obj instanceof Date || obj instanceof RegExp) return obj;
23
+ if (Array.isArray(obj)) return obj.map(walk);
24
+ return Object.keys(obj).reduce(function (acc, key) {
25
+ var camel = key[0].toLowerCase() + key.slice(1).replace(/([A-Z]+)/g, function (m, x) {
26
+ return '_' + x.toLowerCase();
27
+ });
28
+ acc[camel] = walk(obj[key]);
29
+ return acc;
30
+ }, {});
31
+ };
@@ -45,8 +45,9 @@
45
45
  # within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
46
46
  # execution. You don't need to do this for remove, since only the dom id for the model is used.
47
47
  #
48
- # In addition to the four basic actions, you can also use <tt>broadcast_render_later</tt> or
49
- # <tt>broadcast_render_later_to</tt> to render a turbo stream template with multiple actions.
48
+ # In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
49
+ # <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
50
+ # to render a turbo stream template with multiple actions.
50
51
  module Turbo::Broadcastable
51
52
  extend ActiveSupport::Concern
52
53
 
@@ -65,16 +66,22 @@ module Turbo::Broadcastable
65
66
  # belongs_to :board
66
67
  # broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages"
67
68
  # end
68
- def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default)
69
- after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target }
70
- after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream) }
69
+ #
70
+ # class Message < ApplicationRecord
71
+ # belongs_to :board
72
+ # broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
73
+ # end
74
+ 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 }
71
77
  after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) }
72
78
  end
73
79
 
74
- # Same as <tt>#broadcasts_to</tt>, but the designated stream is automatically set to the current model.
75
- def broadcasts(inserts_by: :append, target: broadcast_target_default)
76
- after_create_commit -> { broadcast_action_later action: inserts_by, target: target.try(:call, self) || target }
77
- after_update_commit -> { broadcast_replace_later }
80
+ # Same as <tt>#broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
81
+ # the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
82
+ 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 }
78
85
  after_destroy_commit -> { broadcast_remove }
79
86
  end
80
87
 
@@ -275,9 +282,7 @@ module Turbo::Broadcastable
275
282
  broadcast_action_later_to self, action: action, target: target, **rendering
276
283
  end
277
284
 
278
-
279
- # Render a turbo stream template asynchronously with this broadcastable model passed as the local variable using a
280
- # <tt>Turbo::Streams::BroadcastJob</tt>. Example:
285
+ # Render a turbo stream template with this broadcastable model passed as the local variable. Example:
281
286
  #
282
287
  # # Template: entries/_entry.turbo_stream.erb
283
288
  # <%= turbo_stream.remove entry %>
@@ -289,7 +294,26 @@ module Turbo::Broadcastable
289
294
  # <turbo-stream action="remove" target="entry_5"></turbo-stream>
290
295
  # <turbo-stream action="append" target="entries"><template><div id="entry_5">My Entry</div></template></turbo-stream>
291
296
  #
292
- # ...to the stream named "entry:5"
297
+ # ...to the stream named "entry:5".
298
+ #
299
+ # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
300
+ # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
301
+ # be using `broadcast_render_later`, unless you specifically know why synchronous rendering is needed.
302
+ def broadcast_render(**rendering)
303
+ broadcast_render_to self, **rendering
304
+ end
305
+
306
+ # Same as <tt>broadcast_render</tt> but run with the added option of naming the stream using the passed
307
+ # <tt>streamables</tt>.
308
+ #
309
+ # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
310
+ # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
311
+ # be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
312
+ def broadcast_render_to(*streamables, **rendering)
313
+ Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
314
+ end
315
+
316
+ # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
293
317
  def broadcast_render_later(**rendering)
294
318
  broadcast_render_later_to self, **rendering
295
319
  end
@@ -50,7 +50,7 @@ class Turbo::Streams::TagBuilder
50
50
  action_all :remove, targets, allow_inferred_rendering: false
51
51
  end
52
52
 
53
- # Replace the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
53
+ # Replace the <tt>target</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
54
54
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
55
55
  #
56
56
  # <%= turbo_stream.replace "clearance_5", "<div id='clearance_5'>Replace the dom target identified by clearance_5</div>" %>
@@ -63,7 +63,7 @@ class Turbo::Streams::TagBuilder
63
63
  action :replace, target, content, **rendering, &block
64
64
  end
65
65
 
66
- # Replace the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
66
+ # Replace the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
67
67
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
68
68
  #
69
69
  # <%= turbo_stream.replace_all ".clearance_item", "<div class='clearance_item'>Replace the dom target identified by the class clearance_item</div>" %>
@@ -128,7 +128,7 @@ class Turbo::Streams::TagBuilder
128
128
  action_all :after, targets, content, **rendering, &block
129
129
  end
130
130
 
131
- # Update the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
131
+ # Update the <tt>target</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
132
132
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
133
133
  #
134
134
  # <%= turbo_stream.update "clearance_5", "Update the content of the dom target identified by clearance_5" %>
@@ -141,7 +141,7 @@ class Turbo::Streams::TagBuilder
141
141
  action :update, target, content, **rendering, &block
142
142
  end
143
143
 
144
- # Update the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
144
+ # Update the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
145
145
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the targets as a record. Examples:
146
146
  #
147
147
  # <%= turbo_stream.update_all "clearance_item", "Update the content of the dom target identified by the class clearance_item" %>
@@ -3,7 +3,9 @@ def run_turbo_install_template(path)
3
3
  end
4
4
 
5
5
  def redis_installed?
6
- system('which redis-server > /dev/null')
6
+ Gem.win_platform? ?
7
+ system('where redis-server > NUL 2>&1') :
8
+ system('which redis-server > /dev/null')
7
9
  end
8
10
 
9
11
  def switch_on_redis_if_available
data/lib/turbo/engine.rb CHANGED
@@ -16,8 +16,8 @@ module Turbo
16
16
  #{root}/app/jobs
17
17
  )
18
18
 
19
- initializer "turbo.no_action_cable" do
20
- Rails.autoloaders.once.do_not_eager_load(Dir["#{root}/app/channels/turbo/*_channel.rb"]) unless defined?(ActionCable)
19
+ initializer "turbo.no_action_cable", before: :set_eager_load_paths do
20
+ config.eager_load_paths.delete("#{root}/app/channels") unless defined?(ActionCable)
21
21
  end
22
22
 
23
23
  # If you don't want to precompile Turbo's assets (eg. because you're using webpack),
@@ -7,15 +7,21 @@ module Turbo
7
7
  delegate :dom_id, :dom_class, to: ActionView::RecordIdentifier
8
8
  end
9
9
 
10
- def assert_turbo_stream(action:, target: nil, status: :ok, &block)
10
+ def assert_turbo_stream(action:, target: nil, targets: nil, status: :ok, &block)
11
11
  assert_response status
12
12
  assert_equal Mime[:turbo_stream], response.media_type
13
- assert_select %(turbo-stream[action="#{action}"][target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]), count: 1, &block
13
+ selector = %(turbo-stream[action="#{action}"])
14
+ selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
15
+ selector << %([targets="#{targets}"]) if targets
16
+ assert_select selector, count: 1, &block
14
17
  end
15
18
 
16
- def assert_no_turbo_stream(action:, target: nil)
19
+ def assert_no_turbo_stream(action:, target: nil, targets: nil)
17
20
  assert_equal Mime[:turbo_stream], response.media_type
18
- assert_select %(turbo-stream[action="#{action}"][target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]), count: 0
21
+ selector = %(turbo-stream[action="#{action}"])
22
+ selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
23
+ selector << %([targets="#{targets}"]) if targets
24
+ assert_select selector, count: 0
19
25
  end
20
26
  end
21
27
  end
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "0.9.1"
2
+ VERSION = "1.1.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: 0.9.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,8 +10,22 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-12-14 00:00:00.000000000 Z
13
+ date: 2022-05-22 00:00:00.000000000 Z
14
14
  dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activejob
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 6.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 6.0.0
15
29
  - !ruby/object:Gem::Dependency
16
30
  name: actionpack
17
31
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +80,9 @@ files:
66
80
  - app/helpers/turbo/streams_helper.rb
67
81
  - app/javascript/turbo/cable.js
68
82
  - app/javascript/turbo/cable_stream_source_element.js
83
+ - app/javascript/turbo/form_submissions.js
69
84
  - app/javascript/turbo/index.js
85
+ - app/javascript/turbo/snakeize.js
70
86
  - app/jobs/turbo/streams/action_broadcast_job.rb
71
87
  - app/jobs/turbo/streams/broadcast_job.rb
72
88
  - app/models/concerns/turbo/broadcastable.rb
@@ -99,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
115
  - !ruby/object:Gem::Version
100
116
  version: '0'
101
117
  requirements: []
102
- rubygems_version: 3.2.32
118
+ rubygems_version: 3.2.33
103
119
  signing_key:
104
120
  specification_version: 4
105
121
  summary: The speed of a single-page web application without having to write any JavaScript.