New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Turbo from not triggered when using submit button with form attribute #1246
Comments
If the button is outside the form, it needs a |
Sorry the form attribute was missing in the above example. My point is: Despite the form attribute being set, the form submit is not handled by Turbo. |
@oliverguenther thank you for opening this issue. I've tried to reproduce this in the Turbo test suite with the following changes: diff --git a/src/tests/fixtures/form.html b/src/tests/fixtures/form.html
index aa46bcd..9026e4e 100644
--- a/src/tests/fixtures/form.html
+++ b/src/tests/fixtures/form.html
@@ -15,11 +15,12 @@
<body>
<h1>Form</h1>
<div id="standard">
- <form id="standard-form" action="/__turbo/redirect" method="post" class="redirect">
+ <form id="standard-form" action="/__turbo/redirect" method="post" class="redirect" data-turbo="true">
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<input type="hidden" name="greeting" value="Hello from a redirect">
<input id="standard-post-form-submit" type="submit" value="form[method=post]">
</form>
+ <button id="standard-form-external-submit" form="standard-form">Submit #standard-form</button>
<form action="/__turbo/redirect" method="get" class="redirect">
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<input type="hidden" name="greeting" value="Hello from a redirect">
diff --git a/src/tests/functional/form_submission_tests.js b/src/tests/functional/form_submission_tests.js
index e744794..2a896be 100644
--- a/src/tests/functional/form_submission_tests.js
+++ b/src/tests/functional/form_submission_tests.js
@@ -66,6 +66,14 @@ test("form submission with confirmation cancelled", async ({ page }) => {
assert.notOk(await formSubmitStarted(page))
})
+test("form submission with external submitter", async ({ page }) => {
+ await page.click("#standard-form-external-submit")
+
+ await nextEventOnTarget(page, "standard-form", "turbo:before-fetch-request")
+ await nextEventOnTarget(page, "standard-form", "turbo:submit-start")
+ await nextEventNamed(page, "turbo:load")
+})
+
test("form submission with secondary submitter click - confirmation confirmed", async ({ page }) => {
page.on("dialog", (alert) => {
assert.equal(alert.message(), "Are you really sure?") Unfortunately, I cannot force the test to fail. I've also tried to reproduce it in a single-file Rails application. I'll share the code below: Save this file locally to `bug.rb`, then execute it with `ruby bug.rb`require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "propshaft"
gem "puma"
gem "sqlite3", "~> 1.4"
gem "turbo-rails"
gem "capybara"
gem "cuprite", require: "capybara/cuprite"
end
ENV["DATABASE_URL"] = "sqlite3::memory:"
ENV["RAILS_ENV"] = "test"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
# require "action_mailer/railtie"
# require "active_job/railtie"
require "action_cable/engine"
# require "action_mailbox/engine"
# require "action_text/engine"
require "rails/test_unit/railtie"
class App < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.root = __dir__
config.hosts << "example.org"
config.eager_load = false
config.session_store :cookie_store, key: "cookie_store_key"
config.secret_key_base = "secret_key_base"
config.consider_all_requests_local = true
config.action_cable.cable = {"adapter" => "async"}
config.turbo.draw_routes = false
Rails.logger = config.logger = Logger.new($stdout)
routes.append do
resources :messages, only: [:create, :index]
end
end
Rails.application.initialize!
ActiveRecord::Schema.define do
create_table :messages, force: true do |t|
t.text :body, null: false
end
end
class Message < ActiveRecord::Base
end
class MessagesController < ActionController::Base
include Rails.application.routes.url_helpers
class_attribute :template, default: DATA.read
def create
@message = Message.create!(params.require(:message).permit(:body))
redirect_to action: :index
end
def index
@messages = Message.all
render inline: template, formats: :html
end
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite, using: :chrome, screen_size: [1400, 1400], options: {js_errors: true}
end
Capybara.configure do |config|
config.server = :puma, {Silent: true}
config.default_normalize_ws = true
end
require "rails/test_help"
class TurboSystemTest < ApplicationSystemTestCase
test "reproduces bug" do
visit messages_path
fill_in "Body", with: "Hello, world"
click_button "Create Message"
assert_text "Hello, world"
assert_text "turbo:before-fetch-request"
assert_text "turbo:submit-start"
assert_text "turbo:load"
end
end
__END__
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<script type="importmap">
{
"imports": {
"@hotwired/turbo-rails": "<%= asset_path("turbo.js") %>"
}
}
</script>
<script type="module">
import "@hotwired/turbo-rails"
function log(event) {
document.getElementById("events").insertAdjacentHTML("beforeend", `<p>${event.type}</p>`)
}
addEventListener("turbo:before-fetch-request", log)
addEventListener("turbo:submit-start", log)
addEventListener("turbo:load", log)
</script>
</head>
<body>
<%= form_with model: Message.new, id: "new_message" do |form| %>
<%= form.label :body %>
<%= form.text_area :body %>
<% end %>
<button form="new_message">Create Message</button>
<section id="events" data-turbo-permanent></section>
<section>
<% @messages.each do |message| %>
<p><%= message.body %></p>
<% end %>
</section>
</body>
</html> Could you modify that reproduction script so that it more accurately reflects your scenario, then execute it with |
Hi @seanpdoyle, thanks a lot for looking into this and create a repro. I should have done that to begin with - sorry. The difference is that we're still migrating to Drive, so we have with data-turbo=true on form onlyrequire "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "propshaft"
gem "puma"
gem "sqlite3", "~> 1.4"
gem "turbo-rails"
gem "capybara"
gem "cuprite", require: "capybara/cuprite"
end
ENV["DATABASE_URL"] = "sqlite3::memory:"
ENV["RAILS_ENV"] = "test"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
# require "action_mailer/railtie"
# require "active_job/railtie"
require "action_cable/engine"
# require "action_mailbox/engine"
# require "action_text/engine"
require "rails/test_unit/railtie"
class App < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.root = __dir__
config.hosts << "example.org"
config.eager_load = false
config.session_store :cookie_store, key: "cookie_store_key"
config.secret_key_base = "secret_key_base"
config.consider_all_requests_local = true
config.action_cable.cable = {"adapter" => "async"}
config.turbo.draw_routes = false
Rails.logger = config.logger = Logger.new($stdout)
routes.append do
resources :messages, only: [:create, :index]
end
end
Rails.application.initialize!
ActiveRecord::Schema.define do
create_table :messages, force: true do |t|
t.text :body, null: false
end
end
class Message < ActiveRecord::Base
end
class MessagesController < ActionController::Base
include Rails.application.routes.url_helpers
class_attribute :template, default: DATA.read
def create
@message = Message.create!(params.require(:message).permit(:body))
redirect_to action: :index
end
def index
@messages = Message.all
render inline: template, formats: :html
end
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite, using: :chrome, screen_size: [1400, 1400], options: {js_errors: true}
end
Capybara.configure do |config|
config.server = :puma, {Silent: true}
config.default_normalize_ws = true
end
require "rails/test_help"
class TurboSystemTest < ApplicationSystemTestCase
test "reproduces bug" do
visit messages_path
fill_in "Body", with: "Hello, world"
click_button "Create Message"
assert_text "Hello, world"
assert_text "turbo:before-fetch-request"
assert_text "turbo:submit-start"
assert_text "turbo:load"
end
end
__END__
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<script type="importmap">
{
"imports": {
"@hotwired/turbo-rails": "<%= asset_path("turbo.js") %>"
}
}
</script>
<script type="module">
import "@hotwired/turbo-rails"
function log(event) {
document.getElementById("events").insertAdjacentHTML("beforeend", `<p>${event.type}</p>`)
}
addEventListener("turbo:before-fetch-request", log)
addEventListener("turbo:submit-start", log)
addEventListener("turbo:load", log)
</script>
</head>
<body data-turbo="false">
<%= form_with model: Message.new, data: { turbo: true }, id: "new_message" do |form| %>
<%= form.label :body %>
<%= form.text_area :body %>
<% end %>
<button form="new_message">Create Message</button>
<section id="events" data-turbo-permanent></section>
<section>
<% @messages.each do |message| %>
<p><%= message.body %></p>
<% end %>
</section>
</body>
</html> With this minimal repro, I was able to step through the conditionals and found that the submitter also needs to have with data-turbo on submitterrequire "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "propshaft"
gem "puma"
gem "sqlite3", "~> 1.4"
gem "turbo-rails"
gem "capybara"
gem "cuprite", require: "capybara/cuprite"
end
ENV["DATABASE_URL"] = "sqlite3::memory:"
ENV["RAILS_ENV"] = "test"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
# require "action_mailer/railtie"
# require "active_job/railtie"
require "action_cable/engine"
# require "action_mailbox/engine"
# require "action_text/engine"
require "rails/test_unit/railtie"
class App < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.root = __dir__
config.hosts << "example.org"
config.eager_load = false
config.session_store :cookie_store, key: "cookie_store_key"
config.secret_key_base = "secret_key_base"
config.consider_all_requests_local = true
config.action_cable.cable = {"adapter" => "async"}
config.turbo.draw_routes = false
Rails.logger = config.logger = Logger.new($stdout)
routes.append do
resources :messages, only: [:create, :index]
end
end
Rails.application.initialize!
ActiveRecord::Schema.define do
create_table :messages, force: true do |t|
t.text :body, null: false
end
end
class Message < ActiveRecord::Base
end
class MessagesController < ActionController::Base
include Rails.application.routes.url_helpers
class_attribute :template, default: DATA.read
def create
@message = Message.create!(params.require(:message).permit(:body))
redirect_to action: :index
end
def index
@messages = Message.all
render inline: template, formats: :html
end
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite, using: :chrome, screen_size: [1400, 1400], options: {js_errors: true}
end
Capybara.configure do |config|
config.server = :puma, {Silent: true}
config.default_normalize_ws = true
end
require "rails/test_help"
class TurboSystemTest < ApplicationSystemTestCase
test "reproduces bug" do
visit messages_path
fill_in "Body", with: "Hello, world"
click_button "Create Message"
assert_text "Hello, world"
assert_text "turbo:before-fetch-request"
assert_text "turbo:submit-start"
assert_text "turbo:load"
end
end
__END__
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<script type="importmap">
{
"imports": {
"@hotwired/turbo-rails": "<%= asset_path("turbo.js") %>"
}
}
</script>
<script type="module">
import "@hotwired/turbo-rails"
function log(event) {
document.getElementById("events").insertAdjacentHTML("beforeend", `<p>${event.type}</p>`)
}
addEventListener("turbo:before-fetch-request", log)
addEventListener("turbo:submit-start", log)
addEventListener("turbo:load", log)
</script>
</head>
<body data-turbo="false">
<%= form_with model: Message.new, data: { turbo: true }, id: "new_message" do |form| %>
<%= form.label :body %>
<%= form.text_area :body %>
<% end %>
<button data-turbo="true" form="new_message">Create Message</button>
<section id="events" data-turbo-permanent></section>
<section>
<% @messages.each do |message| %>
<p><%= message.body %></p>
<% end %>
</section>
</body>
</html> With this, I would assume this issue to be rejected. Would be accepting a PR that clarifies this in the docs for future reference in either the doc for data-turbo or the form parts? https://turbo.hotwired.dev/reference/attributes or https://turbo.hotwired.dev/reference/frames ? |
@oliverguenther thank you for sharing more context. Based on intuition alone, the scenario you've shared feels like it should work without In the current implementation, the code is checking that the I think this scenario should be exceptional, and require that the submitter be Turbo-enabled unless it's explicitly opted out with If that makes sense, could you re-open this issue? |
) When using a submitter with [form=id] attribute, it should respect the form's navigatable state unless the submitter explicitly opts out of turbo. Implements the expected behavior outlined in this comment: hotwired#1246 (comment)
Thanks for the clarification. I opened a PR to implement what I understood is your expected behavior in #1251
|
) When using a submitter with [form=id] attribute, it should respect the form's navigatable state unless the submitter explicitly opts out of turbo. Implements the expected behavior outlined in this comment: hotwired#1246 (comment)
Given the following code:
A workaround for that is a global event listener:
I know that's a rather strange use case to have a submit button outside of a form, but at least it's part of the HTML5 spec (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button?retiredLocale=de#form), so I wonder if it would be useful for Turbo to have me provide support in a pull request for it.
The text was updated successfully, but these errors were encountered: