From 422acd04fb09cb97c2965f0e15a21a221c015977 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 9 Apr 2021 10:00:44 -0400 Subject: [PATCH 1/2] Make autoloading changes visible to the DOM Closes [#35][] Related to [#49][] Extend the autoloader to communicate autoloading state via a [DOMTokenList][] stored in the `[data-stimulus-autoloading]` attribute. The goal is to communicate state to the DOM so that tools like Capybara can monitor the presence or absence of the attribute, and synchronize page interactions based on whether autoloading is in-flight. By waiting, our System Tests can act with confidence that any lazily-loaded behavior is ready. [#35]: https://github.com/hotwired/stimulus-rails/issues/35 [#49]: https://github.com/hotwired/stimulus-rails/pull/49/files#r610601520 [DOMTokenList]: https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList --- .../stimulus/loaders/autoloader.js | 32 +++++++++++++++++-- .../controllers/hello_controller.js | 6 ++-- .../namespace/message_rendering_controller.js | 4 +++ .../app/views/application/index.html.erb | 9 ++++++ test/system/autoload_test.rb | 20 ++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/stimulus/loaders/autoloader.js b/app/assets/javascripts/stimulus/loaders/autoloader.js index dd261b6..3ab9649 100644 --- a/app/assets/javascripts/stimulus/loaders/autoloader.js +++ b/app/assets/javascripts/stimulus/loaders/autoloader.js @@ -4,6 +4,30 @@ const application = Application.start() const { controllerAttribute } = application.schema const registeredControllers = {} +function createTokenList(element, attribute) { + const tokenList = document.createElement("div").classList + const tokens = element.getAttribute(attribute) || "" + tokens.split(/\s+/).filter(content => content.length).forEach(token => tokenList.add(token)) + + return tokenList +} + +function addToTokenList(element, attribute, value) { + const tokenList = createTokenList(element, attribute) + tokenList.add(value) + element.setAttribute(attribute, tokenList.toString()) +} + +function removeFromTokenList(element, attribute, value) { + const tokenList = createTokenList(element, attribute) + tokenList.remove(value) + if (tokenList.length) { + element.setAttribute(attribute, tokenList.toString()) + } else { + element.removeAttribute(attribute) + } +} + function autoloadControllersWithin(element) { queryControllerNamesWithin(element).forEach(loadController) } @@ -13,13 +37,16 @@ function queryControllerNamesWithin(element) { } function extractControllerNamesFrom(element) { - return element.getAttribute(controllerAttribute).split(/\s+/).filter(content => content.length) + const tokenList = createTokenList(element, controllerAttribute) + return Array.from(tokenList).map(name => ({ element, name })) } -function loadController(name) { +function loadController({ element, name }) { + addToTokenList(element, "data-stimulus-autoloading", name) import(controllerFilename(name)) .then(module => registerController(name, module)) .catch(error => console.log(`Failed to autoload controller: ${name}`, error)) + .finally(() => removeFromTokenList(element, "data-stimulus-autoloading", name)) } function controllerFilename(name) { @@ -33,7 +60,6 @@ function registerController(name, module) { registeredControllers[name] = true } - new MutationObserver((mutationsList) => { for (const { attributeName, target, type } of mutationsList) { switch (type) { diff --git a/test/dummy/app/assets/javascripts/controllers/hello_controller.js b/test/dummy/app/assets/javascripts/controllers/hello_controller.js index 612a01f..62012c4 100644 --- a/test/dummy/app/assets/javascripts/controllers/hello_controller.js +++ b/test/dummy/app/assets/javascripts/controllers/hello_controller.js @@ -1,7 +1,9 @@ import { Controller } from "stimulus" export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" + static get targets() { return [ "input", "output" ] } + + greet() { + this.outputTarget.innerHTML = `Hello, ${this.inputTarget.value}` } } diff --git a/test/dummy/app/assets/javascripts/controllers/namespace/message_rendering_controller.js b/test/dummy/app/assets/javascripts/controllers/namespace/message_rendering_controller.js index 0f67014..f3b72ef 100644 --- a/test/dummy/app/assets/javascripts/controllers/namespace/message_rendering_controller.js +++ b/test/dummy/app/assets/javascripts/controllers/namespace/message_rendering_controller.js @@ -6,4 +6,8 @@ export default class extends Controller { connect() { this.element.innerHTML = `Namespace: ${this.messageValue}` } + + sayHello() { + this.element.innerHTML = `Hello from Namespace: ${this.messageValue}` + } } diff --git a/test/dummy/app/views/application/index.html.erb b/test/dummy/app/views/application/index.html.erb index f9f288b..2a71ef4 100644 --- a/test/dummy/app/views/application/index.html.erb +++ b/test/dummy/app/views/application/index.html.erb @@ -5,6 +5,15 @@ <%= tag.p "", data: { namespace__message_rendering_message_value: params[:message], controller: " namespace--message-rendering " } %> + +
+ +
+ + +
diff --git a/test/system/autoload_test.rb b/test/system/autoload_test.rb index fdea411..f1529ea 100644 --- a/test/system/autoload_test.rb +++ b/test/system/autoload_test.rb @@ -10,6 +10,16 @@ class AutoloadTest < ApplicationSystemTestCase end end + test "waits for autoloading to complete" do + visit root_path + + within "#eager-loaded" do + click_button "Say Hello" + + assert_text "Hello, Waiting for Eager Load" + end + end + test "autoloads Controller modules on the page lazily" do visit root_path(message: "Hello World!") @@ -45,4 +55,14 @@ class AutoloadTest < ApplicationSystemTestCase assert_text "Namespace: Hello, from Turbo page" end end + + def click_button(*) + begin + assert_css "[data-stimulus-autoloading]" + rescue + # does not eagerly load any Stimulus + end + assert_no_css "[data-stimulus-autoloading]" + super + end end From 6a3e518ca6760241fc41afefdbeb9aa72c90a097 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 9 Apr 2021 10:09:04 -0400 Subject: [PATCH 2/2] Update mimemagic Addresses failure in CI: ``` Fetching gem metadata from https://rubygems.org/............ Your bundle is locked to mimemagic (0.3.5), but that version could not be found in any of the sources listed in your Gemfile. If you haven't changed sources, that means the author of mimemagic (0.3.5) has removed it. You'll need to update your bundle to a version other than mimemagic (0.3.5) that hasn't been removed in order to install. ``` --- Gemfile.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 77711f1..8b765d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,7 +101,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.4) @@ -180,4 +182,4 @@ DEPENDENCIES webdrivers BUNDLED WITH - 2.2.5 + 2.2.12