In my Rails 5.1 app using Turbolinks, I have added a data-disable-with
attribute to my submit buttons, so that on click, the button will be disabled, to prevent accidentally submitting the data multiple times. This works great in many cases.
The problem is that on forms which are submitted via AJAX using the built in UJS helpers (data-remote=true
), when the submit button is clicked, it does not stay disabled. It is initially disabled, but then is re-enabled quickly before the next page has loaded. This defeats the point of the data-disable-with
behaviour, as it enables accidental form re-submission.
Is there a way to keep the form button disabled until the new page has loaded?
Here is the form:
<%= simple_form_for(
resource,
as: resource_name,
url: session_path(resource_name),
html: { class: "large", autocomplete: "on" },
remote: true
) do |f| %>
<%= f.input(
:email,
placeholder: "Email address",
label: false,
autofocus: true
) %>
<%= f.input(:password, placeholder: "Password", label: false) %>
<%= f.button(
:submit,
"Sign in",
class: "ui fluid large teal submit button",
"data-disable-with": "Signing in..."
) %>
<% end %>
data-disable-with
behaviour)We'll need to re-disable the button after step 4. To do this, we'll listen out for the ajax:success
event, and disable it using setTimeout
. This ensures that it will be disabled after Rails has done its thing. (You could use requestAnimationFrame
instead of setTimeout
, but it is not as widely supported.)
To prevent the button from being cached in a disabled state, we'll re-enable it before it is cached. (Note the use of one
rather than on
to prevent the before-cache handler executing more than once.)
I noticed you were using jQuery and jquery-ujs, so I will use functions from those libraries in the code below. Include this somewhere in your main JavaScript file.
jquery-ujs
;(function () {
var $doc = $(document)
$doc.on('submit', 'form[data-remote=true]', function () {
var $form = $(this)
var $button = $form.find('[data-disable-with]')
if (!$button.length) return
$form.on('ajax:complete', function () {
// Use setTimeout to prevent race-condition when Rails re-enables the button
setTimeout(function () {
$.rails.disableFormElement($button)
}, 0)
})
// Prevent button from being cached in disabled state
$doc.one('turbolinks:before-cache', function () {
$.rails.enableFormElement($button)
})
})
})()
rails-ujs / jQuery
;(function () {
var $doc = $(document)
$doc.on('ajax:send', 'form[data-remote=true]', function () {
var $form = $(this)
var $button = $form.find('[data-disable-with]')
if (!$button.length) return
$form.on('ajax:complete', function () {
// Use setTimeout to prevent race-condition when Rails re-enables the button
setTimeout(function () {
$button.each(function () { Rails.disableElement(this) })
}, 0)
})
// Prevent button from being cached in disabled state
$doc.one('turbolinks:before-cache', function () {
$button.each(function () { Rails.enableElement(this) })
})
})
})()
rails-ujs / vanilla JS
Rails.delegate(document, 'form[data-remote=true]', 'ajax:send', function (event) {
var form = event.target
var buttons = form.querySelectorAll('[data-disable-with]')
if (!buttons.length) return
function disableButtons () {
buttons.forEach(function (button) { Rails.disableElement(button) })
}
function enableButtons () {
buttons.forEach(function (button) { Rails.enableElement(button) })
}
function beforeCache () {
enableButtons()
document.removeEventListener('turbolinks:before-cache', beforeCache)
}
form.addEventListener('ajax:complete', function () {
// Use setTimeout to prevent race-condition when Rails re-enables the button
setTimeout(disableButtons, 0)
})
// Prevent button from being cached in disabled state
document.addEventListener('turbolinks:before-cache', beforeCache)
})
Note that this will disable buttons until the next page load on all data-remote
forms with a data-disable-with
button. You may want to change the jQuery selector to only add this behaviour to selected forms.
Hope that helps!