turbo-rails 0.9.1 → 1.1.0

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