From a5645dc0de778bed9bbed8d0280b7fc7873728a6 Mon Sep 17 00:00:00 2001 From: Alex Robbin Date: Thu, 28 Jan 2021 18:52:54 -0500 Subject: [PATCH 001/175] loosen Action Cable NPM package version constraint to allow 6.0.x The gemspec allows Rails 6.0.x, so letting them match up on the NPM side is great. There should be no breaking changes between Action Cable 6.0.x and 6.1.x that would stop this from working. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 787fa8d5..767c0b14 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@hotwired/turbo": "^7.0.0-beta.4", - "@rails/actioncable": "^6.1.0" + "@rails/actioncable": "^6.0.0" }, "devDependencies": { "@rollup/plugin-node-resolve": "^11.0.1", From d71e39af7891b570000c4e94e17257fdfc7248c8 Mon Sep 17 00:00:00 2001 From: Alexandre Ruban Date: Sun, 7 Feb 2021 16:05:32 +0100 Subject: [PATCH 002/175] Enable broadcastable target to be namespaced by parent --- app/models/concerns/turbo/broadcastable.rb | 8 ++++---- test/dummy/app/models/comment.rb | 11 +++++++++++ test/dummy/app/models/message.rb | 4 ++++ test/streams/broadcastable_test.rb | 10 ++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 test/dummy/app/models/comment.rb diff --git a/app/models/concerns/turbo/broadcastable.rb b/app/models/concerns/turbo/broadcastable.rb index cdfafb56..9fb2e9b5 100644 --- a/app/models/concerns/turbo/broadcastable.rb +++ b/app/models/concerns/turbo/broadcastable.rb @@ -52,21 +52,21 @@ module ClassMethods # broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages" # end def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default) - after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target } + after_create_commit -> { broadcast_action_later_to stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target } after_update_commit -> { broadcast_replace_later_to stream.try(:call, self) || send(stream) } after_destroy_commit -> { broadcast_remove_to stream.try(:call, self) || send(stream) } end # Same as #broadcasts_to, but the designated stream is automatically set to the current model. def broadcasts(inserts_by: :append, target: broadcast_target_default) - after_create_commit -> { broadcast_action_later action: inserts_by, target: target } + after_create_commit -> { broadcast_action_later action: inserts_by, target: target.try(:call, self) || target } after_update_commit -> { broadcast_replace_later } after_destroy_commit -> { broadcast_remove } end # All default targets will use the return of this method. Overwrite if you want something else than model_name.plural. def broadcast_target_default - model_name.plural + ->(broadcastable) { broadcastable.send(:broadcast_target_default) } end end @@ -228,7 +228,7 @@ def broadcast_render_later_to(*streamables, **rendering) private def broadcast_target_default - self.class.broadcast_target_default + model_name.plural end def broadcast_rendering_with_defaults(options) diff --git a/test/dummy/app/models/comment.rb b/test/dummy/app/models/comment.rb new file mode 100644 index 00000000..bc423f99 --- /dev/null +++ b/test/dummy/app/models/comment.rb @@ -0,0 +1,11 @@ +class Comment + include ActiveModel::Model + + attr_accessor :record_id, :content, :message + + private + + def broadcast_target_default + "message_#{message.record_id}_comments" + end +end diff --git a/test/dummy/app/models/message.rb b/test/dummy/app/models/message.rb index 3ac07abe..4177722b 100644 --- a/test/dummy/app/models/message.rb +++ b/test/dummy/app/models/message.rb @@ -11,6 +11,10 @@ def initialize(record_id:, content:) @record_id, @content = record_id, content end + def create_comment(content:) + Comment.new(record_id: 1, message: self, content: content) + end + def to_key [ record_id ] end diff --git a/test/streams/broadcastable_test.rb b/test/streams/broadcastable_test.rb index fb57c2db..f0c4c2ba 100644 --- a/test/streams/broadcastable_test.rb +++ b/test/streams/broadcastable_test.rb @@ -84,4 +84,14 @@ class Turbo::BroadcastableTest < ActionCable::Channel::TestCase @profile.broadcast_replace end end + + test "broadcastable target defaults to the pluralized model name" do + assert_equal "messages", @message.send(:broadcast_target_default) + end + + test "broadcastable target can be overriden to be namespaced by parent model" do + comment = @message.create_comment(content: "content") + + assert_equal "message_1_comments", comment.send(:broadcast_target_default) + end end From 7ad7cb09825cbd6df68d6720aa279701de89af6e Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Mon, 8 Feb 2021 09:12:50 -0500 Subject: [PATCH 003/175] Extend Assertions to handle Status Codes Follows https://github.com/hotwired/turbo/pull/168 Since Turbo Stream responses can vary from `200 OK`, change the `assert_turbo_stream` signature to accept a `status:` option (defaulting to `:ok`). Since the inverse `assert_no_turbo_stream` is asserting a particular `action:`, `target:` pairing, omit the `status:` option and remove the assertions `assert_response :ok` call. --- lib/turbo/test_assertions.rb | 5 ++--- test/dummy/app/controllers/messages_controller.rb | 2 +- test/streams/streams_controller_test.rb | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/turbo/test_assertions.rb b/lib/turbo/test_assertions.rb index f9f0944a..cb6dd03a 100644 --- a/lib/turbo/test_assertions.rb +++ b/lib/turbo/test_assertions.rb @@ -7,14 +7,13 @@ module TestAssertions delegate :dom_id, :dom_class, to: ActionView::RecordIdentifier end - def assert_turbo_stream(action:, target: nil, &block) - assert_response :ok + def assert_turbo_stream(action:, target: nil, status: :ok, &block) + assert_response status assert_equal Mime[:turbo_stream], response.media_type assert_select %(turbo-stream[action="#{action}"][target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]), count: 1, &block end def assert_no_turbo_stream(action:, target: nil) - assert_response :ok assert_equal Mime[:turbo_stream], response.media_type assert_select %(turbo-stream[action="#{action}"][target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]), count: 0 end diff --git a/test/dummy/app/controllers/messages_controller.rb b/test/dummy/app/controllers/messages_controller.rb index 00e85c49..31e9bee5 100644 --- a/test/dummy/app/controllers/messages_controller.rb +++ b/test/dummy/app/controllers/messages_controller.rb @@ -6,7 +6,7 @@ def show def create respond_to do |format| format.html { redirect_to message_url(id: 1) } - format.turbo_stream { render turbo_stream: turbo_stream.append(:messages, "message_1") } + format.turbo_stream { render turbo_stream: turbo_stream.append(:messages, "message_1"), status: :created } end end end diff --git a/test/streams/streams_controller_test.rb b/test/streams/streams_controller_test.rb index 5afaaf56..a61de7b9 100644 --- a/test/streams/streams_controller_test.rb +++ b/test/streams/streams_controller_test.rb @@ -6,8 +6,8 @@ class Turbo::StreamsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to message_path(id: 1) post messages_path, as: :turbo_stream - assert_response :ok - assert_turbo_stream action: :append, target: "messages" do |selected| + assert_no_turbo_stream action: :update, target: "messages" + assert_turbo_stream status: :created, action: :append, target: "messages" do |selected| assert_equal "", selected.children.to_html end end From c5af7a7cd15908008ccbe3836b6cb2fbef094c69 Mon Sep 17 00:00:00 2001 From: Eoghain Johnson Date: Tue, 9 Feb 2021 13:02:21 +0000 Subject: [PATCH 004/175] call url_for on given src's on turbo_frame tags --- app/helpers/turbo/frames_helper.rb | 1 + test/dummy/app/models/message.rb | 10 +++++++++- test/frames/frames_helper_test.rb | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/helpers/turbo/frames_helper.rb b/app/helpers/turbo/frames_helper.rb index 44f257e9..38c8a2d7 100644 --- a/app/helpers/turbo/frames_helper.rb +++ b/app/helpers/turbo/frames_helper.rb @@ -25,6 +25,7 @@ module Turbo::FramesHelper # # =>
My tray frame!
def turbo_frame_tag(id, src: nil, target: nil, **attributes, &block) id = id.respond_to?(:to_key) ? dom_id(id) : id + src = url_for(src) if src.present? tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block) end diff --git a/test/dummy/app/models/message.rb b/test/dummy/app/models/message.rb index 3ac07abe..7b13d5a7 100644 --- a/test/dummy/app/models/message.rb +++ b/test/dummy/app/models/message.rb @@ -16,7 +16,7 @@ def to_key end def to_param - "message:#{record_id}" + record_id.to_s end def to_partial_path @@ -30,4 +30,12 @@ def to_s def model_name self.class.model_name end + + def to_model + self + end + + def persisted? + true + end end diff --git a/test/frames/frames_helper_test.rb b/test/frames/frames_helper_test.rb index 1d9d62f9..cd8d0765 100644 --- a/test/frames/frames_helper_test.rb +++ b/test/frames/frames_helper_test.rb @@ -5,6 +5,12 @@ class Turbo::FramesHelperTest < ActionView::TestCase assert_dom_equal %(), turbo_frame_tag("tray", src: "/trays/1") end + test "frame with model src" do + record = Message.new(record_id: "1", content: "ignored") + + assert_dom_equal %(), turbo_frame_tag("message", src: record) + end + test "frame with src and target" do assert_dom_equal %(), turbo_frame_tag("tray", src: "/trays/1", target: "_top") end From 69865446214605986149b1579b5f02e3d71cfd63 Mon Sep 17 00:00:00 2001 From: Javan Makhmali Date: Tue, 2 Feb 2021 10:25:02 -0500 Subject: [PATCH 005/175] Ensure only one consumer instance is created when dynamically importing actioncable.js --- app/assets/javascripts/turbo.js | 18 +++++++++++------- app/javascript/turbo/cable.js | 9 ++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/turbo.js b/app/assets/javascripts/turbo.js index cdf07ae7..20d5df81 100644 --- a/app/assets/javascripts/turbo.js +++ b/app/assets/javascripts/turbo.js @@ -2851,17 +2851,20 @@ var turbo_es2017Esm = Object.freeze({ let consumer; async function getConsumer() { - if (consumer) return consumer; - const {createConsumer: createConsumer} = await Promise.resolve().then((function() { - return index; - })); - return setConsumer(createConsumer()); + return consumer || setConsumer(createConsumer().then(setConsumer)); } function setConsumer(newConsumer) { return consumer = newConsumer; } +async function createConsumer() { + const {createConsumer: createConsumer} = await Promise.resolve().then((function() { + return index; + })); + return createConsumer(); +} + async function subscribeTo(channel, mixin) { const {subscriptions: subscriptions} = await getConsumer(); return subscriptions.create(channel, mixin); @@ -2871,6 +2874,7 @@ var cable = Object.freeze({ __proto__: null, getConsumer: getConsumer, setConsumer: setConsumer, + createConsumer: createConsumer, subscribeTo: subscribeTo }); @@ -3334,7 +3338,7 @@ function createWebSocketURL(url) { } } -function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { +function createConsumer$1(url = getConfig("url") || INTERNAL.default_mount_path) { return new Consumer(url); } @@ -3356,7 +3360,7 @@ var index = Object.freeze({ adapters: adapters, createWebSocketURL: createWebSocketURL, logger: logger, - createConsumer: createConsumer, + createConsumer: createConsumer$1, getConfig: getConfig }); diff --git a/app/javascript/turbo/cable.js b/app/javascript/turbo/cable.js index ace72801..d07d8f8c 100644 --- a/app/javascript/turbo/cable.js +++ b/app/javascript/turbo/cable.js @@ -1,15 +1,18 @@ let consumer export async function getConsumer() { - if (consumer) return consumer - const { createConsumer } = await import("@rails/actioncable/src") - return setConsumer(createConsumer()) + return consumer || setConsumer(createConsumer().then(setConsumer)) } export function setConsumer(newConsumer) { return consumer = newConsumer } +export async function createConsumer() { + const { createConsumer } = await import("@rails/actioncable/src") + return createConsumer() +} + export async function subscribeTo(channel, mixin) { const { subscriptions } = await getConsumer() return subscriptions.create(channel, mixin) From ccfa8ef647d538fe4bcb0e9d775af3e0baf5d39b Mon Sep 17 00:00:00 2001 From: Javan Makhmali Date: Tue, 9 Feb 2021 16:03:09 -0500 Subject: [PATCH 006/175] Add magic comment for webpack consumers https://webpack.js.org/api/module-methods/#magic-comments --- app/javascript/turbo/cable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/turbo/cable.js b/app/javascript/turbo/cable.js index d07d8f8c..0ee62079 100644 --- a/app/javascript/turbo/cable.js +++ b/app/javascript/turbo/cable.js @@ -9,7 +9,7 @@ export function setConsumer(newConsumer) { } export async function createConsumer() { - const { createConsumer } = await import("@rails/actioncable/src") + const { createConsumer } = await import(/* webpackChunkName: "actioncable" */ "@rails/actioncable/src") return createConsumer() } From 62f30de48eb4b3dc1a745643f2bc1e7ddfdcd610 Mon Sep 17 00:00:00 2001 From: Rainer Borene Date: Wed, 10 Feb 2021 11:55:50 -0300 Subject: [PATCH 007/175] fix(engine): ActionCable should not be a requirement --- lib/turbo/engine.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/turbo/engine.rb b/lib/turbo/engine.rb index bd0f6616..b3e27c57 100644 --- a/lib/turbo/engine.rb +++ b/lib/turbo/engine.rb @@ -16,6 +16,10 @@ class Engine < Rails::Engine #{root}/app/jobs ) + initializer "turbo.no_action_cable" do + Rails.autoloaders.once.do_not_eager_load(Dir["#{root}/app/channels/turbo/*_channel.rb"]) unless defined?(ActionCable) + end + initializer "turbo.assets" do if Rails.application.config.respond_to?(:assets) Rails.application.config.assets.precompile += %w( turbo ) From 864a9b92cdc9c75d321a2eda369e6f79e71f4511 Mon Sep 17 00:00:00 2001 From: Javan Makhmali Date: Wed, 10 Feb 2021 16:52:01 -0500 Subject: [PATCH 008/175] Support setting attributes on elements --- app/helpers/turbo/streams_helper.rb | 6 ++++-- test/streams/streams_helper_test.rb | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 test/streams/streams_helper_test.rb diff --git a/app/helpers/turbo/streams_helper.rb b/app/helpers/turbo/streams_helper.rb index 935e8fe2..bfe5706d 100644 --- a/app/helpers/turbo/streams_helper.rb +++ b/app/helpers/turbo/streams_helper.rb @@ -39,7 +39,9 @@ def turbo_stream # The example above will process all turbo streams sent to a stream name like account:5:entries # (when Current.account.id = 5). Updates to this stream can be sent like # entry.broadcast_append_to entry.account, :entries, target: "entries". - def turbo_stream_from(*streamables) - tag.turbo_cable_stream_source channel: "Turbo::StreamsChannel", "signed-stream-name": Turbo::StreamsChannel.signed_stream_name(streamables) + def turbo_stream_from(*streamables, **attributes) + attributes["channel"] = "Turbo::StreamsChannel" + attributes["signed-stream-name"] = Turbo::StreamsChannel.signed_stream_name(streamables) + tag.turbo_cable_stream_source(**attributes) end end diff --git a/test/streams/streams_helper_test.rb b/test/streams/streams_helper_test.rb new file mode 100644 index 00000000..44eb0677 --- /dev/null +++ b/test/streams/streams_helper_test.rb @@ -0,0 +1,15 @@ +require "turbo_test" + +class Turbo::StreamsHelperTest < ActionView::TestCase + test "with streamable" do + assert_dom_equal \ + %(), + turbo_stream_from("messages") + end + + test "with streamable and html attributes" do + assert_dom_equal \ + %(), + turbo_stream_from("messages", data: { stream_target: "source" }) + end +end From 11b2cbe317db5f4f1ece4645b882aff50689a6fe Mon Sep 17 00:00:00 2001 From: robbevp Date: Sat, 13 Feb 2021 10:29:06 +0100 Subject: [PATCH 009/175] Allow require("@rails/activestorage") Changes the install to find either `import ActiveStorage` or `require("@rails/activestorage")` --- lib/install/turbo_with_webpacker.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/install/turbo_with_webpacker.rb b/lib/install/turbo_with_webpacker.rb index a9a0f2a2..cec54a2a 100644 --- a/lib/install/turbo_with_webpacker.rb +++ b/lib/install/turbo_with_webpacker.rb @@ -1,11 +1,12 @@ # Some Rails versions use commonJS(require) others use ESM(import). TURBOLINKS_REGEX = /(import .* from "turbolinks".*\n|require\("turbolinks"\).*\n)/.freeze +ACTIVE_STORAGE_REGEX = /(import.*ActiveStorage|require.*@rails\/activestorage.*)/.freeze abort "❌ Webpacker not found. Exiting." unless defined?(Webpacker::Engine) say "Install Turbo" run "yarn add @hotwired/turbo-rails" -insert_into_file "#{Webpacker.config.source_entry_path}/application.js", "import \"@hotwired/turbo-rails\"\n", before: /import.*ActiveStorage/ +insert_into_file "#{Webpacker.config.source_entry_path}/application.js", "import \"@hotwired/turbo-rails\"\n", before: ACTIVE_STORAGE_REGEX say "Remove Turbolinks" gsub_file 'Gemfile', /gem 'turbolinks'.*/, '' From b30368343696b3446f921aef871897cac8b67347 Mon Sep 17 00:00:00 2001 From: Dayo Esho Date: Mon, 15 Feb 2021 18:04:37 -0800 Subject: [PATCH 010/175] Update broadcastable.rb Fix typo --- app/models/concerns/turbo/broadcastable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/turbo/broadcastable.rb b/app/models/concerns/turbo/broadcastable.rb index cdfafb56..9fb81b23 100644 --- a/app/models/concerns/turbo/broadcastable.rb +++ b/app/models/concerns/turbo/broadcastable.rb @@ -39,7 +39,7 @@ module Turbo::Broadcastable module ClassMethods # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the # stream symbol invocation. By default, the creates are appended to a dom id target name derived from - # the model's plural name. The insertion can also be made to be a prepend by overwriting insertion and + # the model's plural name. The insertion can also be made to be a prepend by overwriting inserts_by and # the target dom id overwritten by passing target. Examples: # # class Message < ApplicationRecord From dd8a8ca0a22d93fdd09f0ba4a096489b11a6567a Mon Sep 17 00:00:00 2001 From: Josh LeBlanc Date: Fri, 26 Feb 2021 07:16:21 -0400 Subject: [PATCH 011/175] run scripts in context of current ruby interpreter Running `bin/bundle` and `bin/yarn` doesn't implicitly run the file with ruby in windows. This will explicitly run the command using the current ruby interpreter. --- lib/install/turbo_with_webpacker.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/install/turbo_with_webpacker.rb b/lib/install/turbo_with_webpacker.rb index a9a0f2a2..dc892eb1 100644 --- a/lib/install/turbo_with_webpacker.rb +++ b/lib/install/turbo_with_webpacker.rb @@ -9,8 +9,8 @@ say "Remove Turbolinks" gsub_file 'Gemfile', /gem 'turbolinks'.*/, '' -run "bin/bundle", capture: true -run "bin/yarn remove turbolinks" +run "#{RbConfig.ruby} bin/bundle", capture: true +run "#{RbConfig.ruby} bin/yarn remove turbolinks" gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, '' gsub_file "#{Webpacker.config.source_entry_path}/application.js", /Turbolinks.start.*\n/, '' From 137bf25f4486f9ab5e478e172f4da7649b74b1f4 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Sun, 28 Feb 2021 23:24:57 +0100 Subject: [PATCH 012/175] run turbo.helpers before config/initializers --- lib/turbo/engine.rb | 2 +- test/dummy/config/initializers/inspect_helpers.rb | 4 ++++ test/initializers/helpers_test.rb | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/dummy/config/initializers/inspect_helpers.rb create mode 100644 test/initializers/helpers_test.rb diff --git a/lib/turbo/engine.rb b/lib/turbo/engine.rb index bd0f6616..c4005154 100644 --- a/lib/turbo/engine.rb +++ b/lib/turbo/engine.rb @@ -22,7 +22,7 @@ class Engine < Rails::Engine end end - initializer "turbo.helpers" do + initializer "turbo.helpers", before: :load_config_initializers do ActiveSupport.on_load(:action_controller_base) do include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation helper Turbo::Engine.helpers diff --git a/test/dummy/config/initializers/inspect_helpers.rb b/test/dummy/config/initializers/inspect_helpers.rb new file mode 100644 index 00000000..f7506adb --- /dev/null +++ b/test/dummy/config/initializers/inspect_helpers.rb @@ -0,0 +1,4 @@ +ActionController::Base.class_eval do + $dummy_ac_base_ancestors_in_initializers = ancestors + $dummy_ac_base_helpers_in_initializers = _helpers.ancestors +end diff --git a/test/initializers/helpers_test.rb b/test/initializers/helpers_test.rb new file mode 100644 index 00000000..4f3c56a2 --- /dev/null +++ b/test/initializers/helpers_test.rb @@ -0,0 +1,8 @@ +require "turbo_test" + +class Turbo::HelpersInInitializersTest < ActionDispatch::IntegrationTest + test "AC::Base has the helpers in place when initializers run" do + assert_includes $dummy_ac_base_ancestors_in_initializers, Turbo::Streams::TurboStreamsTagBuilder + assert_includes $dummy_ac_base_helpers_in_initializers, Turbo::StreamsHelper + end +end From 4229a883934f1019b43af99a6ea9b449c6a58745 Mon Sep 17 00:00:00 2001 From: James Robinson Date: Sun, 7 Mar 2021 20:20:00 +0000 Subject: [PATCH 013/175] Discard broadcast jobs on deserialization error --- app/jobs/turbo/streams/action_broadcast_job.rb | 2 ++ app/jobs/turbo/streams/broadcast_job.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/jobs/turbo/streams/action_broadcast_job.rb b/app/jobs/turbo/streams/action_broadcast_job.rb index a996d33b..a47d7bd7 100644 --- a/app/jobs/turbo/streams/action_broadcast_job.rb +++ b/app/jobs/turbo/streams/action_broadcast_job.rb @@ -1,5 +1,7 @@ # The job that powers all the broadcast_$action_later broadcasts available in Turbo::Streams::Broadcasts. class Turbo::Streams::ActionBroadcastJob < ActiveJob::Base + discard_on ActiveJob::DeserializationError + def perform(stream, action:, target:, **rendering) Turbo::StreamsChannel.broadcast_action_to stream, action: action, target: target, **rendering end diff --git a/app/jobs/turbo/streams/broadcast_job.rb b/app/jobs/turbo/streams/broadcast_job.rb index 38f0a4a6..edfbfe7e 100644 --- a/app/jobs/turbo/streams/broadcast_job.rb +++ b/app/jobs/turbo/streams/broadcast_job.rb @@ -1,6 +1,8 @@ # The job that powers the broadcast_render_later_to available in Turbo::Streams::Broadcasts for rendering # turbo stream templates. class Turbo::Streams::BroadcastJob < ActiveJob::Base + discard_on ActiveJob::DeserializationError + def perform(stream, **rendering) Turbo::StreamsChannel.broadcast_render_to stream, **rendering end From e3d1ba0aae71a1199016cbe24bd2da7d7017c69a Mon Sep 17 00:00:00 2001 From: fig Date: Sat, 3 Apr 2021 00:18:26 +0100 Subject: [PATCH 014/175] Use bundler to remove turbolinks Using bundler to remove turbolinks should be more reliable than using `gsub_file`. The current implementation will fail if double quotes are used in the Gemfile. --- lib/install/turbo_with_webpacker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/install/turbo_with_webpacker.rb b/lib/install/turbo_with_webpacker.rb index a9a0f2a2..2b9e8f29 100644 --- a/lib/install/turbo_with_webpacker.rb +++ b/lib/install/turbo_with_webpacker.rb @@ -8,7 +8,7 @@ insert_into_file "#{Webpacker.config.source_entry_path}/application.js", "import \"@hotwired/turbo-rails\"\n", before: /import.*ActiveStorage/ say "Remove Turbolinks" -gsub_file 'Gemfile', /gem 'turbolinks'.*/, '' +run "bin/bundle remove turbolinks" run "bin/bundle", capture: true run "bin/yarn remove turbolinks" gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, '' From 7bfb625a36c2f7849f4fd738fde6ed9a4e4b3e25 Mon Sep 17 00:00:00 2001 From: fig Date: Tue, 6 Apr 2021 20:17:46 +0100 Subject: [PATCH 015/175] Update bundle because mimemagic --- Gemfile.lock | 142 +++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 72 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a34b596b..cc1b9204 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,60 +7,60 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (6.1.1) - actionpack (= 6.1.1) - activesupport (= 6.1.1) + actioncable (6.1.3.1) + actionpack (= 6.1.3.1) + activesupport (= 6.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.1) - actionpack (= 6.1.1) - activejob (= 6.1.1) - activerecord (= 6.1.1) - activestorage (= 6.1.1) - activesupport (= 6.1.1) + actionmailbox (6.1.3.1) + actionpack (= 6.1.3.1) + activejob (= 6.1.3.1) + activerecord (= 6.1.3.1) + activestorage (= 6.1.3.1) + activesupport (= 6.1.3.1) mail (>= 2.7.1) - actionmailer (6.1.1) - actionpack (= 6.1.1) - actionview (= 6.1.1) - activejob (= 6.1.1) - activesupport (= 6.1.1) + actionmailer (6.1.3.1) + actionpack (= 6.1.3.1) + actionview (= 6.1.3.1) + activejob (= 6.1.3.1) + activesupport (= 6.1.3.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.1) - actionview (= 6.1.1) - activesupport (= 6.1.1) + actionpack (6.1.3.1) + actionview (= 6.1.3.1) + activesupport (= 6.1.3.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.1) - actionpack (= 6.1.1) - activerecord (= 6.1.1) - activestorage (= 6.1.1) - activesupport (= 6.1.1) + actiontext (6.1.3.1) + actionpack (= 6.1.3.1) + activerecord (= 6.1.3.1) + activestorage (= 6.1.3.1) + activesupport (= 6.1.3.1) nokogiri (>= 1.8.5) - actionview (6.1.1) - activesupport (= 6.1.1) + actionview (6.1.3.1) + activesupport (= 6.1.3.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.1) - activesupport (= 6.1.1) + activejob (6.1.3.1) + activesupport (= 6.1.3.1) globalid (>= 0.3.6) - activemodel (6.1.1) - activesupport (= 6.1.1) - activerecord (6.1.1) - activemodel (= 6.1.1) - activesupport (= 6.1.1) - activestorage (6.1.1) - actionpack (= 6.1.1) - activejob (= 6.1.1) - activerecord (= 6.1.1) - activesupport (= 6.1.1) - marcel (~> 0.3.1) - mimemagic (~> 0.3.2) - activesupport (6.1.1) + activemodel (6.1.3.1) + activesupport (= 6.1.3.1) + activerecord (6.1.3.1) + activemodel (= 6.1.3.1) + activesupport (= 6.1.3.1) + activestorage (6.1.3.1) + actionpack (= 6.1.3.1) + activejob (= 6.1.3.1) + activerecord (= 6.1.3.1) + activesupport (= 6.1.3.1) + marcel (~> 1.0.0) + mini_mime (~> 1.0.2) + activesupport (6.1.3.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -69,14 +69,14 @@ GEM addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) builder (3.2.4) - byebug (11.0.1) - capybara (3.34.0) + byebug (11.1.3) + capybara (3.35.3) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.5) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) childprocess (3.0.0) concurrent-ruby (1.1.8) @@ -84,60 +84,58 @@ GEM erubi (1.10.0) globalid (0.4.2) activesupport (>= 4.2.0) - i18n (1.8.7) + i18n (1.8.10) concurrent-ruby (~> 1.0) loofah (2.9.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) + marcel (1.0.1) method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) + mini_mime (1.0.3) mini_portile2 (2.5.0) - minitest (5.14.3) - nio4r (2.5.4) - nokogiri (1.11.1) + minitest (5.14.4) + nio4r (2.5.7) + nokogiri (1.11.2) mini_portile2 (~> 2.5.0) racc (~> 1.4) public_suffix (4.0.6) - puma (5.2.0) + puma (5.2.2) nio4r (~> 2.0) racc (1.5.2) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.1) - actioncable (= 6.1.1) - actionmailbox (= 6.1.1) - actionmailer (= 6.1.1) - actionpack (= 6.1.1) - actiontext (= 6.1.1) - actionview (= 6.1.1) - activejob (= 6.1.1) - activemodel (= 6.1.1) - activerecord (= 6.1.1) - activestorage (= 6.1.1) - activesupport (= 6.1.1) + rails (6.1.3.1) + actioncable (= 6.1.3.1) + actionmailbox (= 6.1.3.1) + actionmailer (= 6.1.3.1) + actionpack (= 6.1.3.1) + actiontext (= 6.1.3.1) + actionview (= 6.1.3.1) + activejob (= 6.1.3.1) + activemodel (= 6.1.3.1) + activerecord (= 6.1.3.1) + activestorage (= 6.1.3.1) + activesupport (= 6.1.3.1) bundler (>= 1.15.0) - railties (= 6.1.1) + railties (= 6.1.3.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.1.1) - actionpack (= 6.1.1) - activesupport (= 6.1.1) + railties (6.1.3.1) + actionpack (= 6.1.3.1) + activesupport (= 6.1.3.1) method_source rake (>= 0.8.7) thor (~> 1.0) - rake (13.0.0) - regexp_parser (1.8.2) - rexml (3.2.4) + rake (13.0.3) + regexp_parser (2.1.1) + rexml (3.2.5) rubyzip (2.3.0) selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) @@ -152,7 +150,7 @@ GEM thor (1.1.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) - webdrivers (4.5.0) + webdrivers (4.6.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (>= 3.0, < 4.0) @@ -177,4 +175,4 @@ DEPENDENCIES webdrivers BUNDLED WITH - 2.2.3 + 2.2.15 From f5e2fb886d01e901df2c38a8817a127ce3a2c0f3 Mon Sep 17 00:00:00 2001 From: Alexandre Ruban Date: Thu, 8 Apr 2021 15:00:19 +0200 Subject: [PATCH 016/175] Reset Turbo::Broadcastable.broadcastable_target_default to model_name.plural --- app/models/concerns/turbo/broadcastable.rb | 4 ++-- test/dummy/app/models/comment.rb | 11 ----------- test/dummy/app/models/message.rb | 4 ---- test/streams/broadcastable_test.rb | 10 ---------- 4 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 test/dummy/app/models/comment.rb diff --git a/app/models/concerns/turbo/broadcastable.rb b/app/models/concerns/turbo/broadcastable.rb index 9fb2e9b5..f64dfd42 100644 --- a/app/models/concerns/turbo/broadcastable.rb +++ b/app/models/concerns/turbo/broadcastable.rb @@ -66,7 +66,7 @@ def broadcasts(inserts_by: :append, target: broadcast_target_default) # All default targets will use the return of this method. Overwrite if you want something else than model_name.plural. def broadcast_target_default - ->(broadcastable) { broadcastable.send(:broadcast_target_default) } + model_name.plural end end @@ -228,7 +228,7 @@ def broadcast_render_later_to(*streamables, **rendering) private def broadcast_target_default - model_name.plural + self.class.broadcast_target_default end def broadcast_rendering_with_defaults(options) diff --git a/test/dummy/app/models/comment.rb b/test/dummy/app/models/comment.rb deleted file mode 100644 index bc423f99..00000000 --- a/test/dummy/app/models/comment.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Comment - include ActiveModel::Model - - attr_accessor :record_id, :content, :message - - private - - def broadcast_target_default - "message_#{message.record_id}_comments" - end -end diff --git a/test/dummy/app/models/message.rb b/test/dummy/app/models/message.rb index 4177722b..3ac07abe 100644 --- a/test/dummy/app/models/message.rb +++ b/test/dummy/app/models/message.rb @@ -11,10 +11,6 @@ def initialize(record_id:, content:) @record_id, @content = record_id, content end - def create_comment(content:) - Comment.new(record_id: 1, message: self, content: content) - end - def to_key [ record_id ] end diff --git a/test/streams/broadcastable_test.rb b/test/streams/broadcastable_test.rb index f0c4c2ba..fb57c2db 100644 --- a/test/streams/broadcastable_test.rb +++ b/test/streams/broadcastable_test.rb @@ -84,14 +84,4 @@ class Turbo::BroadcastableTest < ActionCable::Channel::TestCase @profile.broadcast_replace end end - - test "broadcastable target defaults to the pluralized model name" do - assert_equal "messages", @message.send(:broadcast_target_default) - end - - test "broadcastable target can be overriden to be namespaced by parent model" do - comment = @message.create_comment(content: "content") - - assert_equal "message_1_comments", comment.send(:broadcast_target_default) - end end From 93beec446c91771baf94798da6da3648ed28cae3 Mon Sep 17 00:00:00 2001 From: Alexandre Ruban Date: Thu, 8 Apr 2021 15:28:19 +0200 Subject: [PATCH 017/175] Update mimemagic gem because previous version was yanked --- Gemfile.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a34b596b..2d20f6d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,7 +94,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.5.0) minitest (5.14.3) From 7aa21f44a6960d68de22665947c4487ce6897aa3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 8 Apr 2021 16:25:27 +0200 Subject: [PATCH 018/175] Fix test (closes #130) --- test/drive/drive_helper_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/drive/drive_helper_test.rb b/test/drive/drive_helper_test.rb index 98fdd46c..ba2681d0 100644 --- a/test/drive/drive_helper_test.rb +++ b/test/drive/drive_helper_test.rb @@ -3,6 +3,6 @@ class Turbo::DriveHelperTest < ActionDispatch::IntegrationTest test "opting out of the default cache" do get trays_path - assert_select "meta", name: "turbo-cache-control", content: "no-cache" + assert_match //, @response.body end end From 750abe0aaf3b8ea84d78301647fcab2c1b6f0540 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 8 Apr 2021 16:36:02 +0200 Subject: [PATCH 019/175] Fix installer with missing app layout (closes #145) --- lib/install/turbo_with_asset_pipeline.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/install/turbo_with_asset_pipeline.rb b/lib/install/turbo_with_asset_pipeline.rb index 3e4ad524..e53d7010 100644 --- a/lib/install/turbo_with_asset_pipeline.rb +++ b/lib/install/turbo_with_asset_pipeline.rb @@ -19,12 +19,7 @@ end else say "Default application.html.erb is missing!", :red - - if APPLICATION_LAYOUT_PATH.read =~ /stimulus/ - say %( Add <%= javascript_include_tag("turbo", type: "module-shim") %> and <%= yield :head %> within the tag after Stimulus includes in your custom layout.) - else - say %( Add <%= javascript_include_tag("turbo", type: "module") %> and <%= yield :head %> within the tag in your custom layout.) - end + say %( Add <%= javascript_include_tag("turbo", type: "module-shim") %> and <%= yield :head %> within the tag after Stimulus includes in your custom layout.) end say "Enable redis in bundle" From bfa0ecdb0860191cadb173d9d5fcbf8c4f7327f9 Mon Sep 17 00:00:00 2001 From: Alexandre Ruban Date: Thu, 8 Apr 2021 21:27:40 +0200 Subject: [PATCH 020/175] Add tests for Turbo::Broadcastable .broadcasts and .broadcasts_to --- Gemfile | 1 + Gemfile.lock | 2 + test/dummy/app/models/article.rb | 15 ++++ test/dummy/app/models/comment.rb | 8 +++ .../app/views/articles/_article.html.erb | 1 + .../app/views/comments/_comment.html.erb | 1 + test/dummy/config/application.rb | 2 +- test/dummy/config/database.yml | 12 ++++ .../migrate/20210408163455_create_articles.rb | 9 +++ .../migrate/20210408163514_create_comments.rb | 10 +++ test/dummy/db/schema.rb | 30 ++++++++ test/streams/broadcastable_test.rb | 69 +++++++++++++++++++ 12 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 test/dummy/app/models/article.rb create mode 100644 test/dummy/app/models/comment.rb create mode 100644 test/dummy/app/views/articles/_article.html.erb create mode 100644 test/dummy/app/views/comments/_comment.html.erb create mode 100644 test/dummy/config/database.yml create mode 100644 test/dummy/db/migrate/20210408163455_create_articles.rb create mode 100644 test/dummy/db/migrate/20210408163514_create_comments.rb create mode 100644 test/dummy/db/schema.rb diff --git a/Gemfile b/Gemfile index 0ebf58de..9d2c0764 100644 --- a/Gemfile +++ b/Gemfile @@ -11,4 +11,5 @@ group :test do gem 'rexml' gem 'selenium-webdriver' gem 'webdrivers' + gem 'sqlite3' end diff --git a/Gemfile.lock b/Gemfile.lock index 2d20f6d2..a7b9ae6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,6 +151,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + sqlite3 (1.4.2) thor (1.1.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) @@ -175,6 +176,7 @@ DEPENDENCIES rake rexml selenium-webdriver + sqlite3 turbo-rails! webdrivers diff --git a/test/dummy/app/models/article.rb b/test/dummy/app/models/article.rb new file mode 100644 index 00000000..574ef299 --- /dev/null +++ b/test/dummy/app/models/article.rb @@ -0,0 +1,15 @@ +class Article < ApplicationRecord + has_many :comments + + validates :body, presence: true + + broadcasts target: "overriden-target" + + def to_gid_param + to_param + end + + def to_param + body.parameterize + end +end diff --git a/test/dummy/app/models/comment.rb b/test/dummy/app/models/comment.rb new file mode 100644 index 00000000..c52ef554 --- /dev/null +++ b/test/dummy/app/models/comment.rb @@ -0,0 +1,8 @@ +class Comment < ApplicationRecord + belongs_to :article + + validates :body, presence: true + + broadcasts_to ->(comment) { [comment.article, :comments] }, + target: ->(comment) { "article_#{comment.article_id}_comments" } +end diff --git a/test/dummy/app/views/articles/_article.html.erb b/test/dummy/app/views/articles/_article.html.erb new file mode 100644 index 00000000..78432ffb --- /dev/null +++ b/test/dummy/app/views/articles/_article.html.erb @@ -0,0 +1 @@ +

<%= article.body %>

diff --git a/test/dummy/app/views/comments/_comment.html.erb b/test/dummy/app/views/comments/_comment.html.erb new file mode 100644 index 00000000..d412ee24 --- /dev/null +++ b/test/dummy/app/views/comments/_comment.html.erb @@ -0,0 +1 @@ +

<%= comment.body %>

diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 60cc67e5..fefb2358 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -4,6 +4,7 @@ require "action_cable/engine" require "active_job/railtie" require "active_model/railtie" +require "active_record/railtie" require "sprockets/railtie" Bundler.require(*Rails.groups) @@ -19,4 +20,3 @@ class Application < Rails::Application # the framework and any gems in your application. end end - diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml new file mode 100644 index 00000000..60d1c092 --- /dev/null +++ b/test/dummy/config/database.yml @@ -0,0 +1,12 @@ +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +test: + <<: *default + database: db/test.sqlite3 + +development: + <<: *default + database: db/development.sqlite3 diff --git a/test/dummy/db/migrate/20210408163455_create_articles.rb b/test/dummy/db/migrate/20210408163455_create_articles.rb new file mode 100644 index 00000000..f4ed7eb0 --- /dev/null +++ b/test/dummy/db/migrate/20210408163455_create_articles.rb @@ -0,0 +1,9 @@ +class CreateArticles < ActiveRecord::Migration[6.1] + def change + create_table :articles do |t| + t.text :body, null: false + + t.timestamps + end + end +end diff --git a/test/dummy/db/migrate/20210408163514_create_comments.rb b/test/dummy/db/migrate/20210408163514_create_comments.rb new file mode 100644 index 00000000..6ee9ddfd --- /dev/null +++ b/test/dummy/db/migrate/20210408163514_create_comments.rb @@ -0,0 +1,10 @@ +class CreateComments < ActiveRecord::Migration[6.1] + def change + create_table :comments do |t| + t.text :body, null: false + t.references :article, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb new file mode 100644 index 00000000..874b247d --- /dev/null +++ b/test/dummy/db/schema.rb @@ -0,0 +1,30 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_04_08_163514) do + + create_table "articles", force: :cascade do |t| + t.text "body", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "comments", force: :cascade do |t| + t.text "body", null: false + t.integer "article_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["article_id"], name: "index_comments_on_article_id" + end + + add_foreign_key "comments", "articles" +end diff --git a/test/streams/broadcastable_test.rb b/test/streams/broadcastable_test.rb index fb57c2db..ff8c9de4 100644 --- a/test/streams/broadcastable_test.rb +++ b/test/streams/broadcastable_test.rb @@ -85,3 +85,72 @@ class Turbo::BroadcastableTest < ActionCable::Channel::TestCase end end end + +class Turbo::BroadcastableArticleTest < ActionCable::Channel::TestCase + include ActiveJob::TestHelper, Turbo::Streams::ActionHelper + + test "creating an article broadcasts to the overriden target with a string" do + assert_broadcast_on "body", turbo_stream_action_tag("append", target: "overriden-target", template: "

Body

\n") do + perform_enqueued_jobs do + Article.create!(body: "Body") + end + end + end + + test "updating an article broadcasts" do + article = Article.create!(body: "Hey") + + assert_broadcast_on "ho", turbo_stream_action_tag("replace", target: "article_#{article.id}", template: "

Ho

\n") do + perform_enqueued_jobs do + article.update!(body: "Ho") + end + end + end + + test "destroying an article broadcasts" do + article = Article.create!(body: "Hey") + + assert_broadcast_on "hey", turbo_stream_action_tag("remove", target: "article_#{article.id}") do + article.destroy! + end + end +end + +class Turbo::BroadcastableCommentTest < ActionCable::Channel::TestCase + include ActiveJob::TestHelper, Turbo::Streams::ActionHelper + + setup { @article = Article.create!(body: "Body") } + + test "creating a comment broadcasts to the overriden target with a lambda" do + stream = "#{@article.to_gid_param}:comments" + target = "article_#{@article.id}_comments" + + assert_broadcast_on stream, turbo_stream_action_tag("append", target: target, template: "

comment

\n") do + perform_enqueued_jobs do + @article.comments.create!(body: "comment") + end + end + end + + test "updating a comment broadcasts" do + comment = @article.comments.create!(body: "random") + stream = "#{@article.to_gid_param}:comments" + target = "comment_#{comment.id}" + + assert_broadcast_on stream, turbo_stream_action_tag("replace", target: target, template: "

precise

\n") do + perform_enqueued_jobs do + comment.update!(body: "precise") + end + end + end + + test "destroying a comment broadcasts" do + comment = @article.comments.create!(body: "comment") + stream = "#{@article.to_gid_param}:comments" + target = "comment_#{comment.id}" + + assert_broadcast_on stream, turbo_stream_action_tag("remove", target: target) do + comment.destroy! + end + end +end From 6e48c64fcea855a00066f3fea49c4fab2c6f31e2 Mon Sep 17 00:00:00 2001 From: Alexandre Ruban Date: Fri, 9 Apr 2021 09:50:52 +0200 Subject: [PATCH 021/175] Add a setup database step in CI workflow --- .github/workflows/ci.yml | 4 +++- Rakefile | 4 ++++ bin/rails | 13 +++++++++++++ test/turbo_test.rb | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 bin/rails diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5712b37..9392faad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,6 @@ jobs: bundler-cache: true - name: Run tests - run: bundle exec rake + run: | + bundle exec rails db:test:prepare + bundle exec rails test diff --git a/Rakefile b/Rakefile index f0baf501..a7b8a9a9 100644 --- a/Rakefile +++ b/Rakefile @@ -2,6 +2,10 @@ require "bundler/setup" require "bundler/gem_tasks" require "rake/testtask" +APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) +load "rails/tasks/engine.rake" +load "rails/tasks/statistics.rake" + Rake::TestTask.new do |test| test.libs << "test" test.test_files = FileList["test/**/*_test.rb"] diff --git a/bin/rails b/bin/rails new file mode 100644 index 00000000..de173dd1 --- /dev/null +++ b/bin/rails @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + +ENGINE_ROOT = File.expand_path('..', __dir__) +APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + +require 'rails/all' +require 'rails/engine/commands' diff --git a/test/turbo_test.rb b/test/turbo_test.rb index 4cb0bc9e..f872c34a 100644 --- a/test/turbo_test.rb +++ b/test/turbo_test.rb @@ -7,6 +7,7 @@ require_relative "dummy/config/environment" +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] ActionCable.server.config.logger = Logger.new(STDOUT) if ENV["VERBOSE"] class ActiveSupport::TestCase From 54b057ed3f6314cec88a98561481293201e47e72 Mon Sep 17 00:00:00 2001 From: Alexandre Ruban Date: Fri, 9 Apr 2021 11:00:53 +0200 Subject: [PATCH 022/175] Don't forget system tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9392faad..18b7d0b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,4 @@ jobs: - name: Run tests run: | bundle exec rails db:test:prepare - bundle exec rails test + bundle exec rake From c08b60bab9b2349bc9665e7ef4cdb4ddba579834 Mon Sep 17 00:00:00 2001 From: Javan Makhmali Date: Wed, 14 Apr 2021 13:48:16 -0400 Subject: [PATCH 023/175] Sync yarn.lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0b04ff7d..e272d97a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.0.0-beta.4.tgz#be6d7014902d45121f56358d360e0810bad4c19e" integrity sha512-koox7UeA2Na0tLBPesxNO3nk/gtezRxF33NHKbAx+zv5s2k1Xh/orB5xNr5dAckoUj3Pdpytnw41WH4QB1jYBg== -"@rails/actioncable@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.0.tgz#f336f25450b1bc43b99bc60557a70b6e6bb1d3d2" - integrity sha512-eDgy+vcKN9RIzxmMBfSAe77rTj2cp6kJALiVQyKrW2O9EK2MdostOmP+99At/Dit3ur5+77NVnruxD7y14ZYFA== +"@rails/actioncable@^6.0.0": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.3.tgz#c8a67ec4d22ecd6931f7ebd98143fddbc815419a" + integrity sha512-m02524MR9cTnUNfGz39Lkx9jVvuL0tle4O7YgvouJ7H83FILxzG1nQ5jw8pAjLAr9XQGu+P1sY4SKE3zyhCNjw== "@rollup/plugin-node-resolve@^11.0.1": version "11.0.1" From f0b58a21e7addae0eaeea839df1416363ffc404f Mon Sep 17 00:00:00 2001 From: Javan Makhmali Date: Wed, 14 Apr 2021 13:53:29 -0400 Subject: [PATCH 024/175] Sync Gemfile.lock --- Gemfile.lock | 3 --- 1 file changed, 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 16681da6..71e628e1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,9 +93,6 @@ GEM mini_mime (>= 0.1.1) marcel (1.0.1) method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake mini_mime (1.0.3) mini_portile2 (2.5.0) minitest (5.14.4) From 2945dcb7c5fc634ad7d459e0872cf04cf8a3576d Mon Sep 17 00:00:00 2001 From: Javan Makhmali Date: Wed, 14 Apr 2021 14:02:12 -0400 Subject: [PATCH 025/175] Bump Turbo to 7.0.0-beta.5 https://github.com/hotwired/turbo/releases/tag/v7.0.0-beta.5 --- app/assets/javascripts/turbo.js | 1295 +++++++++++++++++-------------- package.json | 2 +- yarn.lock | 8 +- 3 files changed, 701 insertions(+), 604 deletions(-) diff --git a/app/assets/javascripts/turbo.js b/app/assets/javascripts/turbo.js index 20d5df81..0f8cc195 100644 --- a/app/assets/javascripts/turbo.js +++ b/app/assets/javascripts/turbo.js @@ -55,7 +55,7 @@ class FrameElement extends HTMLElement { this.delegate = new FrameElement.delegateConstructor(this); } static get observedAttributes() { - return [ "loading", "src" ]; + return [ "disabled", "loading", "src" ]; } connectedCallback() { this.delegate.connect(); @@ -68,6 +68,8 @@ class FrameElement extends HTMLElement { this.delegate.loadingStyleChanged(); } else if (name == "src") { this.delegate.sourceURLChanged(); + } else { + this.delegate.disabledChanged(); } } get src() { @@ -133,9 +135,7 @@ function frameLoadingStyleFromString(style) { } function expandURL(locatable) { - const anchor = document.createElement("a"); - anchor.href = locatable.toString(); - return new URL(anchor.href); + return new URL(locatable.toString(), document.baseURI); } function getAnchor(url) { @@ -171,6 +171,10 @@ function toCacheKey(url) { } } +function urlsAreEqual(left, right) { + return expandURL(left).href == expandURL(right).href; +} + function getPathComponents(url) { return url.pathname.split("/").slice(1); } @@ -323,6 +327,7 @@ class FetchRequest { this.abortController = new AbortController; this.delegate = delegate; this.method = method; + this.headers = this.defaultHeaders; if (this.isIdempotent) { this.url = mergeFormDataEntries(location, [ ...body.entries() ]); } else { @@ -343,7 +348,9 @@ class FetchRequest { this.abortController.abort(); } async perform() { + var _a, _b; const {fetchOptions: fetchOptions} = this; + (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this); dispatch("turbo:before-fetch-request", { detail: { fetchOptions: fetchOptions @@ -387,24 +394,17 @@ class FetchRequest { signal: this.abortSignal }; } + get defaultHeaders() { + return { + Accept: "text/html, application/xhtml+xml" + }; + } get isIdempotent() { return this.method == FetchMethod.get; } - get headers() { - const headers = Object.assign({}, this.defaultHeaders); - if (typeof this.delegate.prepareHeadersForRequest == "function") { - this.delegate.prepareHeadersForRequest(headers, this); - } - return headers; - } get abortSignal() { return this.abortController.signal; } - get defaultHeaders() { - return { - Accept: "text/html, application/xhtml+xml" - }; - } } function mergeFormDataEntries(url, entries) { @@ -548,6 +548,9 @@ class FormSubmission { var _a; return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype); } + get isIdempotent() { + return this.fetchRequest.isIdempotent; + } get stringFormData() { return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []); } @@ -639,8 +642,8 @@ function buildFormData(formElement, submitter) { const formData = new FormData(formElement); const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name"); const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value"); - if (name && formData.get(name) != value) { - formData.append(name, value || ""); + if (name && value != null && formData.get(name) != value) { + formData.append(name, value); } return formData; } @@ -682,6 +685,9 @@ class Snapshot { return null; } } + get isConnected() { + return this.element.isConnected; + } get firstAutofocusableElement() { return this.element.querySelector("[autofocus]"); } @@ -691,8 +697,16 @@ class Snapshot { getPermanentElementById(id) { return this.element.querySelector(`#${id}[data-turbo-permanent]`); } - getPermanentElementsPresentInSnapshot(snapshot) { - return this.permanentElements.filter((({id: id}) => snapshot.getPermanentElementById(id))); + getPermanentElementMapForSnapshot(snapshot) { + const permanentElementMap = {}; + for (const currentPermanentElement of this.permanentElements) { + const {id: id} = currentPermanentElement; + const newPermanentElement = snapshot.getPermanentElementById(id); + if (newPermanentElement) { + permanentElementMap[id] = [ currentPermanentElement, newPermanentElement ]; + } + } + return permanentElementMap; } } @@ -837,6 +851,56 @@ class LinkInterceptor { } } +class Bardo { + constructor(permanentElementMap) { + this.permanentElementMap = permanentElementMap; + } + static preservingPermanentElements(permanentElementMap, callback) { + const bardo = new this(permanentElementMap); + bardo.enter(); + callback(); + bardo.leave(); + } + enter() { + for (const id in this.permanentElementMap) { + const [, newPermanentElement] = this.permanentElementMap[id]; + this.replaceNewPermanentElementWithPlaceholder(newPermanentElement); + } + } + leave() { + for (const id in this.permanentElementMap) { + const [currentPermanentElement] = this.permanentElementMap[id]; + this.replaceCurrentPermanentElementWithClone(currentPermanentElement); + this.replacePlaceholderWithPermanentElement(currentPermanentElement); + } + } + replaceNewPermanentElementWithPlaceholder(permanentElement) { + const placeholder = createPlaceholderForPermanentElement(permanentElement); + permanentElement.replaceWith(placeholder); + } + replaceCurrentPermanentElementWithClone(permanentElement) { + const clone = permanentElement.cloneNode(true); + permanentElement.replaceWith(clone); + } + replacePlaceholderWithPermanentElement(permanentElement) { + const placeholder = this.getPlaceholderById(permanentElement.id); + placeholder === null || placeholder === void 0 ? void 0 : placeholder.replaceWith(permanentElement); + } + getPlaceholderById(id) { + return this.placeholders.find((element => element.content == id)); + } + get placeholders() { + return [ ...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]") ]; + } +} + +function createPlaceholderForPermanentElement(permanentElement) { + const element = document.createElement("meta"); + element.setAttribute("name", "turbo-permanent-placeholder"); + element.setAttribute("content", permanentElement.id); + return element; +} + class Renderer { constructor(currentSnapshot, newSnapshot, isPreview) { this.currentSnapshot = currentSnapshot; @@ -871,28 +935,25 @@ class Renderer { } } preservingPermanentElements(callback) { - const placeholders = relocatePermanentElements(this.currentSnapshot, this.newSnapshot); - callback(); - replacePlaceholderElementsWithClonedPermanentElements(placeholders); + Bardo.preservingPermanentElements(this.permanentElementMap, callback); } focusFirstAutofocusableElement() { - const element = this.newSnapshot.firstAutofocusableElement; + const element = this.connectedSnapshot.firstAutofocusableElement; if (elementIsFocusable(element)) { element.focus(); } } + get connectedSnapshot() { + return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot; + } get currentElement() { return this.currentSnapshot.element; } get newElement() { return this.newSnapshot.element; } -} - -function replaceElementWithElement(fromElement, toElement) { - const parentElement = fromElement.parentElement; - if (parentElement) { - return parentElement.replaceChild(toElement, fromElement); + get permanentElementMap() { + return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot); } } @@ -902,37 +963,6 @@ function copyElementAttributes(destinationElement, sourceElement) { } } -function createPlaceholderForPermanentElement(permanentElement) { - const element = document.createElement("meta"); - element.setAttribute("name", "turbo-permanent-placeholder"); - element.setAttribute("content", permanentElement.id); - return { - element: element, - permanentElement: permanentElement - }; -} - -function replacePlaceholderElementsWithClonedPermanentElements(placeholders) { - for (const {element: element, permanentElement: permanentElement} of placeholders) { - const clonedElement = permanentElement.cloneNode(true); - replaceElementWithElement(element, clonedElement); - } -} - -function relocatePermanentElements(currentSnapshot, newSnapshot) { - return currentSnapshot.getPermanentElementsPresentInSnapshot(newSnapshot).reduce(((placeholders, permanentElement) => { - const newElement = newSnapshot.getPermanentElementById(permanentElement.id); - if (newElement) { - const placeholder = createPlaceholderForPermanentElement(permanentElement); - replaceElementWithElement(permanentElement, placeholder.element); - replaceElementWithElement(newElement, permanentElement); - return [ ...placeholders, placeholder ]; - } else { - return placeholders; - } - }), []); -} - function elementIsFocusable(element) { return element && typeof element.focus == "function"; } @@ -985,458 +1015,133 @@ function readScrollLogicalPosition(value, defaultValue) { } } -class FrameController { - constructor(element) { - this.resolveVisitPromise = () => {}; - this.element = element; - this.view = new FrameView(this, this.element); - this.appearanceObserver = new AppearanceObserver(this, this.element); - this.linkInterceptor = new LinkInterceptor(this, this.element); - this.formInterceptor = new FormInterceptor(this, this.element); - } - connect() { - if (this.loadingStyle == FrameLoadingStyle.lazy) { - this.appearanceObserver.start(); - } - this.linkInterceptor.start(); - this.formInterceptor.start(); - } - disconnect() { - this.appearanceObserver.stop(); - this.linkInterceptor.stop(); - this.formInterceptor.stop(); - } - sourceURLChanged() { - if (this.loadingStyle == FrameLoadingStyle.eager) { - this.loadSourceURL(); - } - } - loadingStyleChanged() { - if (this.loadingStyle == FrameLoadingStyle.lazy) { - this.appearanceObserver.start(); - } else { - this.appearanceObserver.stop(); - this.loadSourceURL(); - } +class ProgressBar { + constructor() { + this.hiding = false; + this.value = 0; + this.visible = false; + this.trickle = () => { + this.setValue(this.value + Math.random() / 100); + }; + this.stylesheetElement = this.createStylesheetElement(); + this.progressElement = this.createProgressElement(); + this.installStylesheetElement(); + this.setValue(0); } - async loadSourceURL() { - if (this.isActive && this.sourceURL && this.sourceURL != this.loadingURL) { - try { - this.loadingURL = this.sourceURL; - this.element.loaded = this.visit(this.sourceURL); - this.appearanceObserver.stop(); - await this.element.loaded; - } finally { - delete this.loadingURL; + static get defaultCSS() { + return unindent` + .turbo-progress-bar { + position: fixed; + display: block; + top: 0; + left: 0; + height: 3px; + background: #0076ff; + z-index: 9999; + transition: + width ${ProgressBar.animationDuration}ms ease-out, + opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in; + transform: translate3d(0, 0, 0); } + `; + } + show() { + if (!this.visible) { + this.visible = true; + this.installProgressElement(); + this.startTrickling(); } } - async loadResponse(response) { - try { - const html = await response.responseHTML; - if (html) { - const {body: body} = parseHTMLDocument(html); - const snapshot = new Snapshot(await this.extractForeignFrameElement(body)); - const renderer = new FrameRenderer(this.view.snapshot, snapshot, false); - await this.view.render(renderer); - } - } catch (error) { - console.error(error); - this.view.invalidate(); + hide() { + if (this.visible && !this.hiding) { + this.hiding = true; + this.fadeProgressElement((() => { + this.uninstallProgressElement(); + this.stopTrickling(); + this.visible = false; + this.hiding = false; + })); } } - elementAppearedInViewport(element) { - this.loadSourceURL(); + setValue(value) { + this.value = value; + this.refresh(); } - shouldInterceptLinkClick(element, url) { - return this.shouldInterceptNavigation(element); + installStylesheetElement() { + document.head.insertBefore(this.stylesheetElement, document.head.firstChild); } - linkClickIntercepted(element, url) { - this.navigateFrame(element, url); + installProgressElement() { + this.progressElement.style.width = "0"; + this.progressElement.style.opacity = "1"; + document.documentElement.insertBefore(this.progressElement, document.body); + this.refresh(); } - shouldInterceptFormSubmission(element) { - return this.shouldInterceptNavigation(element); + fadeProgressElement(callback) { + this.progressElement.style.opacity = "0"; + setTimeout(callback, ProgressBar.animationDuration * 1.5); } - formSubmissionIntercepted(element, submitter) { - if (this.formSubmission) { - this.formSubmission.stop(); + uninstallProgressElement() { + if (this.progressElement.parentNode) { + document.documentElement.removeChild(this.progressElement); } - this.formSubmission = new FormSubmission(this, element, submitter); - if (this.formSubmission.fetchRequest.isIdempotent) { - this.navigateFrame(element, this.formSubmission.fetchRequest.url.href); - } else { - this.formSubmission.start(); + } + startTrickling() { + if (!this.trickleInterval) { + this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration); } } - prepareHeadersForRequest(headers, request) { - headers["Turbo-Frame"] = this.id; + stopTrickling() { + window.clearInterval(this.trickleInterval); + delete this.trickleInterval; } - requestStarted(request) { - this.element.setAttribute("busy", ""); + refresh() { + requestAnimationFrame((() => { + this.progressElement.style.width = `${10 + this.value * 90}%`; + })); } - requestPreventedHandlingResponse(request, response) { - this.resolveVisitPromise(); + createStylesheetElement() { + const element = document.createElement("style"); + element.type = "text/css"; + element.textContent = ProgressBar.defaultCSS; + return element; } - async requestSucceededWithResponse(request, response) { - await this.loadResponse(response); - this.resolveVisitPromise(); + createProgressElement() { + const element = document.createElement("div"); + element.className = "turbo-progress-bar"; + return element; } - requestFailedWithResponse(request, response) { - console.error(response); - this.resolveVisitPromise(); +} + +ProgressBar.animationDuration = 300; + +class HeadSnapshot extends Snapshot { + constructor() { + super(...arguments); + this.detailsByOuterHTML = this.children.reduce(((result, element) => { + const {outerHTML: outerHTML} = element; + const details = outerHTML in result ? result[outerHTML] : { + type: elementType(element), + tracked: elementIsTracked(element), + elements: [] + }; + return Object.assign(Object.assign({}, result), { + [outerHTML]: Object.assign(Object.assign({}, details), { + elements: [ ...details.elements, element ] + }) + }); + }), {}); } - requestErrored(request, error) { - console.error(error); - this.resolveVisitPromise(); + get trackedElementSignature() { + return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join(""); } - requestFinished(request) { - this.element.removeAttribute("busy"); + getScriptElementsNotInSnapshot(snapshot) { + return this.getElementsMatchingTypeNotInSnapshot("script", snapshot); } - formSubmissionStarted(formSubmission) {} - formSubmissionSucceededWithResponse(formSubmission, response) { - const frame = this.findFrameElement(formSubmission.formElement); - frame.delegate.loadResponse(response); + getStylesheetElementsNotInSnapshot(snapshot) { + return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot); } - formSubmissionFailedWithResponse(formSubmission, fetchResponse) { - this.element.delegate.loadResponse(fetchResponse); - } - formSubmissionErrored(formSubmission, error) {} - formSubmissionFinished(formSubmission) {} - viewWillRenderSnapshot(snapshot, isPreview) {} - viewRenderedSnapshot(snapshot, isPreview) {} - viewInvalidated() {} - async visit(url) { - const request = new FetchRequest(this, FetchMethod.get, expandURL(url)); - return new Promise((resolve => { - this.resolveVisitPromise = () => { - this.resolveVisitPromise = () => {}; - resolve(); - }; - request.perform(); - })); - } - navigateFrame(element, url) { - const frame = this.findFrameElement(element); - frame.src = url; - } - findFrameElement(element) { - var _a; - const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target"); - return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element; - } - async extractForeignFrameElement(container) { - let element; - const id = CSS.escape(this.id); - if (element = activateElement(container.querySelector(`turbo-frame#${id}`))) { - return element; - } - if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`))) { - await element.loaded; - return await this.extractForeignFrameElement(element); - } - console.error(`Response has no matching element`); - return new FrameElement; - } - shouldInterceptNavigation(element) { - const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target"); - if (!this.enabled || id == "_top") { - return false; - } - if (id) { - const frameElement = getFrameElementById(id); - if (frameElement) { - return !frameElement.disabled; - } - } - return true; - } - get id() { - return this.element.id; - } - get enabled() { - return !this.element.disabled; - } - get sourceURL() { - return this.element.src; - } - get loadingStyle() { - return this.element.loading; - } - get isLoading() { - return this.formSubmission !== undefined || this.loadingURL !== undefined; - } - get isActive() { - return this.element.isActive; - } -} - -function getFrameElementById(id) { - if (id != null) { - const element = document.getElementById(id); - if (element instanceof FrameElement) { - return element; - } - } -} - -function activateElement(element) { - if (element && element.ownerDocument !== document) { - element = document.importNode(element, true); - } - if (element instanceof FrameElement) { - return element; - } -} - -const StreamActions = { - append() { - var _a; - (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.append(this.templateContent); - }, - prepend() { - var _a; - (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.prepend(this.templateContent); - }, - remove() { - var _a; - (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.remove(); - }, - replace() { - var _a; - (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.replaceWith(this.templateContent); - }, - update() { - if (this.targetElement) { - this.targetElement.innerHTML = ""; - this.targetElement.append(this.templateContent); - } - } -}; - -class StreamElement extends HTMLElement { - async connectedCallback() { - try { - await this.render(); - } catch (error) { - console.error(error); - } finally { - this.disconnect(); - } - } - async render() { - var _a; - return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => { - if (this.dispatchEvent(this.beforeRenderEvent)) { - await nextAnimationFrame(); - this.performAction(); - } - })(); - } - disconnect() { - try { - this.remove(); - } catch (_a) {} - } - get performAction() { - if (this.action) { - const actionFunction = StreamActions[this.action]; - if (actionFunction) { - return actionFunction; - } - this.raise("unknown action"); - } - this.raise("action attribute is missing"); - } - get targetElement() { - var _a; - if (this.target) { - return (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target); - } - this.raise("target attribute is missing"); - } - get templateContent() { - return this.templateElement.content; - } - get templateElement() { - if (this.firstElementChild instanceof HTMLTemplateElement) { - return this.firstElementChild; - } - this.raise("first child element must be a