turbo-rails 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +24 -0
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +147 -0
- data/MIT-LICENSE +20 -0
- data/README.md +66 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/turbo.js +3161 -0
- data/app/channels/turbo/streams/broadcasts.rb +66 -0
- data/app/channels/turbo/streams/stream_name.rb +24 -0
- data/app/channels/turbo/streams_channel.rb +17 -0
- data/app/controllers/turbo/frames/frame_request.rb +24 -0
- data/app/controllers/turbo/native/navigation.rb +49 -0
- data/app/controllers/turbo/native/navigation_controller.rb +13 -0
- data/app/controllers/turbo/streams/turbo_streams_tag_builder.rb +22 -0
- data/app/helpers/turbo/drive_helper.rb +16 -0
- data/app/helpers/turbo/frames_helper.rb +23 -0
- data/app/helpers/turbo/includes_helper.rb +5 -0
- data/app/helpers/turbo/streams/action_helper.rb +25 -0
- data/app/helpers/turbo/streams_helper.rb +22 -0
- data/app/javascript/turbo/cable.js +16 -0
- data/app/javascript/turbo/cable_stream_source_element.js +27 -0
- data/app/javascript/turbo/index.js +3 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +6 -0
- data/app/jobs/turbo/streams/broadcast_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +236 -0
- data/app/models/turbo/streams/tag_builder.rb +127 -0
- data/config/routes.rb +6 -0
- data/lib/install/turbo.rb +11 -0
- data/lib/tasks/turbo_tasks.rake +6 -0
- data/lib/turbo-rails.rb +17 -0
- data/lib/turbo/engine.rb +65 -0
- data/lib/turbo/test_assertions.rb +22 -0
- data/lib/turbo/version.rb +3 -0
- data/package.json +42 -0
- data/rollup.config.js +23 -0
- data/test/drive/drive_helper_test.rb +8 -0
- data/test/dummy/.babelrc +18 -0
- data/test/dummy/.gitignore +3 -0
- data/test/dummy/.postcssrc.yml +3 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +2 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/assets/stylesheets/scaffold.css +80 -0
- data/test/dummy/app/channels/application_cable/channel.rb +4 -0
- data/test/dummy/app/channels/application_cable/connection.rb +4 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/messages_controller.rb +12 -0
- data/test/dummy/app/controllers/trays_controller.rb +17 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/javascript/packs/application.js +0 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailboxes/application_mailbox.rb +2 -0
- data/test/dummy/app/mailboxes/messages_mailbox.rb +4 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/message.rb +29 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/app/views/messages/_message.html.erb +1 -0
- data/test/dummy/app/views/messages/_message.turbo_stream.erb +1 -0
- data/test/dummy/app/views/messages/show.turbo_stream.erb +9 -0
- data/test/dummy/app/views/trays/index.html.erb +3 -0
- data/test/dummy/app/views/trays/show.html.erb +3 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +36 -0
- data/test/dummy/bin/update +31 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +22 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +34 -0
- data/test/dummy/config/environments/production.rb +96 -0
- data/test/dummy/config/environments/test.rb +38 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +22 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +34 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config/webpack/development.js +3 -0
- data/test/dummy/config/webpack/environment.js +3 -0
- data/test/dummy/config/webpack/production.js +3 -0
- data/test/dummy/config/webpack/test.js +3 -0
- data/test/dummy/config/webpacker.yml +65 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/frames/frame_request_controller_test.rb +21 -0
- data/test/frames/frames_helper_test.rb +15 -0
- data/test/native/navigation_controller_test.rb +42 -0
- data/test/streams/broadcastable_test.rb +80 -0
- data/test/streams/streams_channel_test.rb +105 -0
- data/test/streams/streams_controller_test.rb +29 -0
- data/test/turbo_test.rb +10 -0
- data/turbo-rails.gemspec +16 -0
- data/yarn.lock +282 -0
- metadata +254 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
# Provides the broadcast actions in synchronous and asynchrous form for the <tt>Turbo::StreamsChannel</tt>.
|
2
|
+
# See <tt>Turbo::Broadcastable</tt> for the user-facing API that invokes these methods with most of the paperwork filled out already.
|
3
|
+
#
|
4
|
+
# Can be used directly using something like <tt>Turbo::StreamsChannel.broadcast_remove_to :entries, target: 1</tt>.
|
5
|
+
module Turbo::Streams::Broadcasts
|
6
|
+
include Turbo::Streams::ActionHelper
|
7
|
+
|
8
|
+
def broadcast_remove_to(*streamables, target:)
|
9
|
+
broadcast_action_to *streamables, action: :remove, target: target
|
10
|
+
end
|
11
|
+
|
12
|
+
def broadcast_replace_to(*streamables, target:, **rendering)
|
13
|
+
broadcast_action_to *streamables, action: :replace, target: target, **rendering
|
14
|
+
end
|
15
|
+
|
16
|
+
def broadcast_append_to(*streamables, target:, **rendering)
|
17
|
+
broadcast_action_to *streamables, action: :append, target: target, **rendering
|
18
|
+
end
|
19
|
+
|
20
|
+
def broadcast_prepend_to(*streamables, target:, **rendering)
|
21
|
+
broadcast_action_to *streamables, action: :prepend, target: target, **rendering
|
22
|
+
end
|
23
|
+
|
24
|
+
def broadcast_action_to(*streamables, action:, target:, **rendering)
|
25
|
+
broadcast_stream_to *streamables, content: turbo_stream_action_tag(action, target: target, template:
|
26
|
+
rendering.delete(:content) || (rendering.any? ? render_format(:html, **rendering) : nil)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def broadcast_replace_later_to(*streamables, target:, **rendering)
|
32
|
+
broadcast_action_later_to *streamables, action: :replace, target: target, **rendering
|
33
|
+
end
|
34
|
+
|
35
|
+
def broadcast_append_later_to(*streamables, target:, **rendering)
|
36
|
+
broadcast_action_later_to *streamables, action: :append, target: target, **rendering
|
37
|
+
end
|
38
|
+
|
39
|
+
def broadcast_prepend_later_to(*streamables, target:, **rendering)
|
40
|
+
broadcast_action_later_to *streamables, action: :prepend, target: target, **rendering
|
41
|
+
end
|
42
|
+
|
43
|
+
def broadcast_action_later_to(*streamables, action:, target:, **rendering)
|
44
|
+
Turbo::Streams::ActionBroadcastJob.perform_later \
|
45
|
+
stream_name_from(streamables), action: action, target: target, **rendering
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def broadcast_render_to(*streamables, **rendering)
|
50
|
+
broadcast_stream_to *streamables, content: render_format(:turbo_stream, **rendering)
|
51
|
+
end
|
52
|
+
|
53
|
+
def broadcast_render_later_to(*streamables, **rendering)
|
54
|
+
Turbo::Streams::BroadcastJob.perform_later stream_name_from(streamables), **rendering
|
55
|
+
end
|
56
|
+
|
57
|
+
def broadcast_stream_to(*streamables, content:)
|
58
|
+
ActionCable.server.broadcast stream_name_from(streamables), content
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
private
|
63
|
+
def render_format(format, **rendering)
|
64
|
+
ApplicationController.render(formats: [ format ], **rendering)
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Stream names are how we identify which updates should go to which users. All streams run over the same
|
2
|
+
# <tt>Turbo::StreamsChannel</tt>, but each with their own subscription. Since stream names are exposed directly to the user
|
3
|
+
# via the HTML stream subscription tags, we need to ensure that the name isn't tampered with, so the names are signed
|
4
|
+
# upon generation and verified upon receipt. All verification happens through the <tt>Turbo.signed_stream_verifier</tt>.
|
5
|
+
module Turbo::Streams::StreamName
|
6
|
+
# Used by <tt>Turbo::StreamsChannel</tt> to verify a signed stream name.
|
7
|
+
def verified_stream_name(signed_stream_name)
|
8
|
+
Turbo.signed_stream_verifier.verified signed_stream_name
|
9
|
+
end
|
10
|
+
|
11
|
+
# Used by <tt>Turbo::StreamsHelper#turbo_stream_from(*streamables)</tt> to generate a signed stream name.
|
12
|
+
def signed_stream_name(streamables)
|
13
|
+
Turbo.signed_stream_verifier.generate stream_name_from(streamables)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def stream_name_from(streamables)
|
18
|
+
if streamables.is_a?(Array)
|
19
|
+
streamables.map { |streamable| stream_name_from(streamable) }.join(":")
|
20
|
+
else
|
21
|
+
streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# The streams channel delivers all the turbo-stream actions created (primarily) through <tt>Turbo::Broadcastable</tt>.
|
2
|
+
# A subscription to this channel is made for each individual stream that one wishes to listen for updates to.
|
3
|
+
# The subscription relies on being passed a <tt>signed_stream_name</tt> parameter generated by turning a set of streamables
|
4
|
+
# into signed stream name using <tt>Turbo::Streams::StreamName#signed_stream_name</tt>. This is automatically done
|
5
|
+
# using the view helper <tt>Turbo::StreamsHelper#turbo_stream_from(*streamables)</tt>.
|
6
|
+
# If the signed stream name cannot be verified, the subscription is rejected.
|
7
|
+
class Turbo::StreamsChannel < ActionCable::Channel::Base
|
8
|
+
extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
|
9
|
+
|
10
|
+
def subscribed
|
11
|
+
if verified_stream_name = self.class.verified_stream_name(params[:signed_stream_name])
|
12
|
+
stream_from verified_stream_name
|
13
|
+
else
|
14
|
+
reject
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Turbo frame requests are requests made from within a turbo frame with the intention of replacing the content of just
|
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).
|
7
|
+
#
|
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.
|
10
|
+
#
|
11
|
+
# This module is automatically included in <tt>ActionController::Base</tt>.
|
12
|
+
module Turbo::Frames::FrameRequest
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
layout -> { false if turbo_frame_request? }
|
17
|
+
etag { :frame if turbo_frame_request? }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def turbo_frame_request?
|
22
|
+
request.headers["Turbo-Frame"].present?
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Turbo is built to work with native navigation principles and present those alongside what's required for the web. When you
|
2
|
+
# have Turbo Native clients running (see the Turbo iOS and Turbo Android projects for details), you can respond to native
|
3
|
+
# requests with three dedicated responses: <tt>recede</tt>, <tt>resume</tt>, <tt>refresh</tt>.
|
4
|
+
#
|
5
|
+
# FIXME: Supply full description of when we use either.
|
6
|
+
module Turbo::Native::Navigation
|
7
|
+
private
|
8
|
+
|
9
|
+
def recede_or_redirect_to(url, **options)
|
10
|
+
turbo_native_action_or_redirect url, :recede, :to, options
|
11
|
+
end
|
12
|
+
|
13
|
+
def resume_or_redirect_to(url, **options)
|
14
|
+
turbo_native_action_or_redirect url, :resume, :to, options
|
15
|
+
end
|
16
|
+
|
17
|
+
def refresh_or_redirect_to(url, **options)
|
18
|
+
turbo_native_action_or_redirect url, :refresh, :to, options
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def recede_or_redirect_back_or_to(url, **options)
|
23
|
+
turbo_native_action_or_redirect url, :recede, :back, options
|
24
|
+
end
|
25
|
+
|
26
|
+
def resume_or_redirect_back_or_to(url, **options)
|
27
|
+
turbo_native_action_or_redirect url, :resume, :back, options
|
28
|
+
end
|
29
|
+
|
30
|
+
def refresh_or_redirect_back_or_to(url, **options)
|
31
|
+
turbo_native_action_or_redirect url, :refresh, :back, options
|
32
|
+
end
|
33
|
+
|
34
|
+
# :nodoc:
|
35
|
+
def turbo_native_action_or_redirect(url, action, redirect_type, options = {})
|
36
|
+
if turbo_native_app?
|
37
|
+
redirect_to send("turbo_#{action}_historical_location_url", notice: options[:notice] || options.delete(:native_notice))
|
38
|
+
elsif redirect_type == :back
|
39
|
+
redirect_back fallback_location: url, **options
|
40
|
+
else
|
41
|
+
redirect_to url, options
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Turbo Native applications are identified by having the string "Turbo Native" as part of their user agent.
|
46
|
+
def turbo_native_app?
|
47
|
+
request.user_agent.to_s.match?(/Turbo Native/)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Most turbo streams are rendered either asynchronously via <tt>Turbo::Broadcastable</tt>/<tt>Turbo::StreamsChannel</tt> or
|
2
|
+
# rendered in templates with the <tt>turbo_stream.erb</tt> extension. But it's also possible to render updates inline
|
3
|
+
# in controllers, like so:
|
4
|
+
#
|
5
|
+
# def destroy
|
6
|
+
# @user.destroy!
|
7
|
+
#
|
8
|
+
# respond_to do |format|
|
9
|
+
# format.turbo_stream { render turbo_stream: turbo_stream.remove(@user) }
|
10
|
+
# format.html { redirect_to users_url, notice: "User removed" }
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# This module adds that turbo_stream tag-builder object to all controllers. It's an instance of <tt>Turbo::Streams::TagBuilder</tt>
|
15
|
+
# instantiated with the current <tt>view_context</tt>.
|
16
|
+
module Turbo::Streams::TurboStreamsTagBuilder
|
17
|
+
private
|
18
|
+
|
19
|
+
def turbo_stream
|
20
|
+
Turbo::Streams::TagBuilder.new(view_context)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
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.
|
4
|
+
#
|
5
|
+
# ==== Example
|
6
|
+
#
|
7
|
+
# # app/views/application.html.erb
|
8
|
+
# <html><head><%= yield :head %></head><body><%= yield %></html>
|
9
|
+
#
|
10
|
+
# # app/views/trays/index.html.erb
|
11
|
+
# <% turbo_exempts_page_from_cache %>
|
12
|
+
# <p>Page that shouldn't be cached by Turbo</p>
|
13
|
+
def turbo_exempts_page_from_cache
|
14
|
+
provide :head, %(<meta name="turbo-cache-control" content="no-cache">).html_safe
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Turbo::FramesHelper
|
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
|
+
# fetches the URL supplied in the +src+ attribute.
|
4
|
+
#
|
5
|
+
# === Examples
|
6
|
+
#
|
7
|
+
# # => turbo-frame id="tray" src="http://example.com/trays/1"></turbo-frame>
|
8
|
+
# <%= turbo_frame_tag "tray", src: tray_path(tray) %>
|
9
|
+
#
|
10
|
+
# # => turbo-frame id="tray" links-target="top" src="http://example.com/trays/1"></turbo-frame>
|
11
|
+
# <%= turbo_frame_tag "tray", src: tray_path(tray), links_target: "top" %>
|
12
|
+
#
|
13
|
+
# # => turbo-frame id="tray" links-target="other_tray"></turbo-frame>
|
14
|
+
# <%= turbo_frame_tag "tray", links_target: "other_tray" %>
|
15
|
+
#
|
16
|
+
# # => turbo-frame id="tray"><div>My tray frame!</div></turbo-frame>
|
17
|
+
# <%= turbo_frame_tag "tray" do %>
|
18
|
+
# <div>My tray frame!</div>
|
19
|
+
# <% end %>
|
20
|
+
def turbo_frame_tag(id, src: nil, target: nil, **attributes, &block)
|
21
|
+
tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Turbo::Streams::ActionHelper
|
2
|
+
# Creates a `turbo-stream` tag according to the passed parameters. Examples:
|
3
|
+
#
|
4
|
+
# # => <turbo-stream action="remove" target="message_1"></turbo-stream>
|
5
|
+
# turbo_stream_action_tag "remove", target: "message_1"
|
6
|
+
#
|
7
|
+
# # => <turbo-stream action="replace" target="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
|
8
|
+
# turbo_stream_action_tag "replace", target: "message_1", template: %(<div id="message_1">Hello!</div>)
|
9
|
+
def turbo_stream_action_tag(action, target:, template: nil)
|
10
|
+
target = convert_to_turbo_stream_dom_id(target)
|
11
|
+
template = template ? "<template>#{template}</template>" : ""
|
12
|
+
|
13
|
+
%(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def convert_to_turbo_stream_dom_id(element_or_dom_id)
|
18
|
+
if element_or_dom_id.respond_to?(:to_key)
|
19
|
+
element = element_or_dom_id
|
20
|
+
ActionView::RecordIdentifier.dom_id(element)
|
21
|
+
else
|
22
|
+
dom_id = element_or_dom_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Turbo::StreamsHelper
|
2
|
+
# Returns a new <tt>Turbo::Streams::TagBuilder</tt> object that accepts stream actions and renders them them as
|
3
|
+
# the template tags needed to send across the wire. This object is automatically yielded to turbo_stream.erb templates.
|
4
|
+
def turbo_stream
|
5
|
+
Turbo::Streams::TagBuilder.new(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Used in the view to create a subscription to a stream identified by the <tt>streamables</tt> running over the
|
9
|
+
# <tt>Turbo::StreamsChannel</tt>. The stream name being generated is safe to embed in the HTML sent to a user without
|
10
|
+
# fear of tampering, as it is signed using <tt>Turbo.signed_stream_verifier</tt>. Example:
|
11
|
+
#
|
12
|
+
# # app/views/entries/index.html.erb
|
13
|
+
# <%= turbo_stream_from Current.account, :entries %>
|
14
|
+
# <div id="entries">New entries will be appended to this container</div>
|
15
|
+
#
|
16
|
+
# The example above will process all turbo streams sent to a stream name like <tt>account:5:entries</tt>
|
17
|
+
# (when Current.account.id = 5). Updates to this stream can be sent like
|
18
|
+
# <tt>entry.broadcast_append_to entry.account, :entries, contrainer: "entries"</tt>.
|
19
|
+
def turbo_stream_from(*streamables)
|
20
|
+
tag.turbo_cable_stream_source channel: "Turbo::StreamsChannel", "signed-stream-name": Turbo::StreamsChannel.signed_stream_name(streamables)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
let consumer
|
2
|
+
|
3
|
+
export async function getConsumer() {
|
4
|
+
if (consumer) return consumer
|
5
|
+
const { createConsumer } = await import("@rails/actioncable/src")
|
6
|
+
return setConsumer(createConsumer())
|
7
|
+
}
|
8
|
+
|
9
|
+
export function setConsumer(newConsumer) {
|
10
|
+
return consumer = newConsumer
|
11
|
+
}
|
12
|
+
|
13
|
+
export async function subscribeTo(channel, mixin) {
|
14
|
+
const { subscriptions } = await getConsumer()
|
15
|
+
return subscriptions.create(channel, mixin)
|
16
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
|
2
|
+
import { subscribeTo } from "./cable"
|
3
|
+
|
4
|
+
class TurboCableStreamSourceElement extends HTMLElement {
|
5
|
+
async connectedCallback() {
|
6
|
+
connectStreamSource(this)
|
7
|
+
this.subscription = subscribeTo(this.channel, { received: this.dispatchMessageEvent.bind(this) })
|
8
|
+
}
|
9
|
+
|
10
|
+
disconnectedCallback() {
|
11
|
+
disconnectStreamSource(this)
|
12
|
+
if (this.subscription) this.subscription.unsubscribe()
|
13
|
+
}
|
14
|
+
|
15
|
+
dispatchMessageEvent(data) {
|
16
|
+
const event = new MessageEvent("message", { data })
|
17
|
+
return this.dispatchEvent(event)
|
18
|
+
}
|
19
|
+
|
20
|
+
get channel() {
|
21
|
+
const channel = this.getAttribute("channel")
|
22
|
+
const signed_stream_name = this.getAttribute("signed-stream-name")
|
23
|
+
return { channel, signed_stream_name }
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# The job that powers all the <tt>broadcast_$action_later</tt> broadcasts available in <tt>Turbo::Streams::Broadcasts</tt>.
|
2
|
+
class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base
|
3
|
+
def perform(stream, action:, target:, **rendering)
|
4
|
+
Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering
|
5
|
+
end
|
6
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# The job that powers the <tt>broadcast_render_later_to</tt> available in <tt>Turbo::Streams::Broadcasts</tt> for rendering
|
2
|
+
# turbo stream templates.
|
3
|
+
class Turbo::Streams::BroadcastJob < ActiveJob::Base
|
4
|
+
def perform(stream, **rendering)
|
5
|
+
Turbo::StreamsChannel.broadcast_render_to stream, **rendering
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# Turbo streams can be broadcast directly from models that include this module (this is automatically done for Active Records).
|
2
|
+
# This makes it convenient to execute both synchronous and asynchronous updates, and render directly from callbacks in models
|
3
|
+
# or from controllers or jobs that act on those models. Here's an example:
|
4
|
+
#
|
5
|
+
# class Clearance < ApplicationRecord
|
6
|
+
# belongs_to :petitioner, class_name: "Contact"
|
7
|
+
# belongs_to :examiner, class_name: "User"
|
8
|
+
#
|
9
|
+
# after_create_commit :broadcast_later
|
10
|
+
#
|
11
|
+
# private
|
12
|
+
# def broadcast_later
|
13
|
+
# broadcast_prepend_later_to examiner.identity, :clearances
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# This is an example from [HEY](https://hey.com), and the clearance is the model that drives
|
18
|
+
# [the screener](https://hey.com/features/the-screener/), which gives users the power to deny first-time senders (petitioners)
|
19
|
+
# access to their attention (as the examiner). When a new clearance is created upon receipt of an email from a first-time
|
20
|
+
# sender, that'll trigger the call to broadcast_later, which in turn invokes <tt>broadcast_prepend_later_to</tt>.
|
21
|
+
#
|
22
|
+
# That method enqueues a <tt>Turbo::Streams::ActionBroadcastJob</tt> for the prepend, which will render the partial for clearance
|
23
|
+
# (it knows which by calling Clearance#to_partial_path, which in this case returns <tt>clearances/_clearance.html.erb</tt>),
|
24
|
+
# send that to all users that have subscribed to updates (using <tt>turbo_stream_from(examiner.identity, :clearances)</tt> in a view)
|
25
|
+
# using the <tt>Turbo::StreamsChannel</tt> under the stream name derived from <tt>[ examiner.identity, :clearances ]</tt>,
|
26
|
+
# and finally prepend the result of that partial rendering to the target identified with the dom id "clearances"
|
27
|
+
# (which is derived by default from the plural model name of the model, but can be overwritten).
|
28
|
+
#
|
29
|
+
# There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
|
30
|
+
# <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
|
31
|
+
# within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
|
32
|
+
# execution. You don't need to do this for remove, since only the dom id for the model is used.
|
33
|
+
#
|
34
|
+
# In addition to the four basic actions, you can also use <tt>broadcast_render_later</tt> or
|
35
|
+
# <tt>broadcast_render_later_to</tt> to render a turbo stream template with multiple actions.
|
36
|
+
module Turbo::Broadcastable
|
37
|
+
extend ActiveSupport::Concern
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
# Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
|
41
|
+
# <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
|
42
|
+
# the model's plural name. The insertion can also be made to be a prepend by overwriting <tt>insertion</tt> and
|
43
|
+
# the target dom id overwritten by passing <tt>target</tt>. Examples:
|
44
|
+
#
|
45
|
+
# class Message < ApplicationRecord
|
46
|
+
# belongs_to :board
|
47
|
+
# broadcasts_to :board
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# class Message < ApplicationRecord
|
51
|
+
# belongs_to :board
|
52
|
+
# broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages"
|
53
|
+
# end
|
54
|
+
def broadcasts_to(stream, inserts_by: :append, target: model_name.plural)
|
55
|
+
after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target }
|
56
|
+
after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream) }
|
57
|
+
after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Same as <tt>#broadcasts_to</tt>, but the designated stream is automatically set to the current model.
|
61
|
+
def broadcasts(inserts_by: :append, target: model_name.plural)
|
62
|
+
after_create_commit -> { broadcast_action_later action: inserts_by, target: target }
|
63
|
+
after_update_commit -> { broadcast_replace_later }
|
64
|
+
after_destroy_commit -> { broadcast_remove }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
|
69
|
+
# Example:
|
70
|
+
#
|
71
|
+
# # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
|
72
|
+
# clearance.broadcast_remove_to examiner.identity, :clearances
|
73
|
+
def broadcast_remove_to(*streamables)
|
74
|
+
Turbo::StreamsChannel.broadcast_remove_to *streamables, target: self
|
75
|
+
end
|
76
|
+
|
77
|
+
# Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
|
78
|
+
def broadcast_remove
|
79
|
+
broadcast_remove_to self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed
|
83
|
+
# <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
|
84
|
+
#
|
85
|
+
# # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
86
|
+
# # to the stream named "identity:2:clearances"
|
87
|
+
# clearance.broadcast_replace_to examiner.identity, :clearances
|
88
|
+
#
|
89
|
+
# # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
|
90
|
+
# # to the stream named "identity:2:clearances"
|
91
|
+
# clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
92
|
+
def broadcast_replace_to(*streamables, **rendering)
|
93
|
+
Turbo::StreamsChannel.broadcast_replace_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
|
97
|
+
def broadcast_replace(**rendering)
|
98
|
+
broadcast_replace_to self, **rendering
|
99
|
+
end
|
100
|
+
|
101
|
+
# Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
|
102
|
+
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
|
103
|
+
# appending named arguments to the call. Examples:
|
104
|
+
#
|
105
|
+
# # Sends <turbo-stream action="append" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
106
|
+
# # to the stream named "identity:2:clearances"
|
107
|
+
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances"
|
108
|
+
#
|
109
|
+
# # Sends <turbo-stream action="append" target="clearances"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
|
110
|
+
# # to the stream named "identity:2:clearances"
|
111
|
+
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
|
112
|
+
# partial: "clearances/other_partial", locals: { a: 1 }
|
113
|
+
def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
|
114
|
+
Turbo::StreamsChannel.broadcast_append_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
|
118
|
+
def broadcast_append(target: broadcast_target_default, **rendering)
|
119
|
+
broadcast_append_to self, target: target, **rendering
|
120
|
+
end
|
121
|
+
|
122
|
+
# Prepend a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
|
123
|
+
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
|
124
|
+
# appending named arguments to the call. Examples:
|
125
|
+
#
|
126
|
+
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
127
|
+
# # to the stream named "identity:2:clearances"
|
128
|
+
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances"
|
129
|
+
#
|
130
|
+
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
|
131
|
+
# # to the stream named "identity:2:clearances"
|
132
|
+
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
|
133
|
+
# partial: "clearances/other_partial", locals: { a: 1 }
|
134
|
+
def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
|
135
|
+
Turbo::StreamsChannel.broadcast_prepend_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
|
139
|
+
def broadcast_prepend(target: broadcast_target_default, **rendering)
|
140
|
+
broadcast_prepend_to self, target: target, **rendering
|
141
|
+
end
|
142
|
+
|
143
|
+
# Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
|
144
|
+
#
|
145
|
+
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
146
|
+
# # to the stream named "identity:2:clearances"
|
147
|
+
# clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
|
148
|
+
def broadcast_action_to(*streamables, action:, target: broadcast_target_default, **rendering)
|
149
|
+
Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
|
150
|
+
end
|
151
|
+
|
152
|
+
# Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
|
153
|
+
def broadcast_action(action, target: broadcast_target_default, **rendering)
|
154
|
+
broadcast_action_to self, action: action, target: target, **rendering
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
# Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
159
|
+
def broadcast_replace_later_to(*streamables, **rendering)
|
160
|
+
Turbo::StreamsChannel.broadcast_replace_later_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
|
164
|
+
def broadcast_replace_later(**rendering)
|
165
|
+
broadcast_replace_later_to self, **rendering
|
166
|
+
end
|
167
|
+
|
168
|
+
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
169
|
+
def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
|
170
|
+
Turbo::StreamsChannel.broadcast_append_later_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
|
174
|
+
def broadcast_append_later(target: broadcast_target_default, **rendering)
|
175
|
+
broadcast_append_later_to self, target: target, **rendering
|
176
|
+
end
|
177
|
+
|
178
|
+
# Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
179
|
+
def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering)
|
180
|
+
Turbo::StreamsChannel.broadcast_prepend_later_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
|
184
|
+
def broadcast_prepend_later(target: broadcast_target_default, **rendering)
|
185
|
+
broadcast_prepend_later_to self, target: target, **rendering
|
186
|
+
end
|
187
|
+
|
188
|
+
# Same as <tt>broadcast_action_later_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
189
|
+
def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, **rendering)
|
190
|
+
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
|
191
|
+
end
|
192
|
+
|
193
|
+
# Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
|
194
|
+
def broadcast_action_later(action:, target: broadcast_target_default, **rendering)
|
195
|
+
broadcast_action_later_to self, action: action, target: target, **rendering
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# Render a turbo stream template asynchronously with this broadcastable model passed as the local variable using a
|
200
|
+
# <tt>Turbo::Streams::BroadcastJob</tt>. Example:
|
201
|
+
#
|
202
|
+
# # Template: entries/_entry.turbo_stream.erb
|
203
|
+
# <%= turbo_stream.remove entry %>
|
204
|
+
#
|
205
|
+
# <%= turbo_stream.append "entries" do %>
|
206
|
+
# <%= render partial: "entries/entry", locals: { entry: entry }, formats: [ :html ] %>
|
207
|
+
# <% end if entry.active? %>
|
208
|
+
#
|
209
|
+
# # Sends:
|
210
|
+
# # <turbo-stream action="remove" target="entry_5"></turbo-stream>
|
211
|
+
# # <turbo-stream action="append" target="entries"><template><div id="entry_5">My Entry</div></template></turbo-stream>
|
212
|
+
# # to the stream named "entry:5"
|
213
|
+
# entry.broadcast_render_later
|
214
|
+
def broadcast_render_later(**rendering)
|
215
|
+
broadcast_render_later_to self, **rendering
|
216
|
+
end
|
217
|
+
|
218
|
+
# Same as <tt>broadcast_prepend_to</tt> but run with the added option of naming the stream using the passed
|
219
|
+
# <tt>streamables</tt>.
|
220
|
+
def broadcast_render_later_to(*streamables, **rendering)
|
221
|
+
Turbo::StreamsChannel.broadcast_render_later_to *streamables, **broadcast_rendering_with_defaults(rendering)
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
private
|
226
|
+
def broadcast_target_default
|
227
|
+
model_name.plural
|
228
|
+
end
|
229
|
+
|
230
|
+
def broadcast_rendering_with_defaults(options)
|
231
|
+
options.tap do |o|
|
232
|
+
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.singular.to_sym => self)
|
233
|
+
o[:partial] ||= to_partial_path
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|