10

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 %>

Here's what happens. Example of submit button disabling properly on click, but then re-enabling before new page is loaded

Patrick O'Grady
  • 548
  • 4
  • 13
  • 1
    Are we to intuit your code from that animation? – jvillian Oct 19 '17 at 14:44
  • 1
    @jvillian @patrick-ogrady has noted in the description that he is using the `data-remote=true` attribute on the form, as well as the `data-disable-with` attribute on the button, which I feel is sufficient from a code perspective. It was missing a clear question, which I hope I have helped to clarify. – Dom Christie Oct 19 '17 at 18:10
  • thanks @DomChristie, I have also added the code for the form. – Patrick O'Grady Oct 19 '17 at 18:34

3 Answers3

15

What is happening?

  1. Form is submitted
  2. rails-ujs disables the button (the data-disable-with behaviour)
  3. The form request succeeds
  4. rails-ujs re-enables the button
  5. turbolinks-rails makes a request to the redirect location (which might be a slow one, leaving the button in an enabled state)

Solution

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!

Dom Christie
  • 4,152
  • 3
  • 23
  • 31
  • Thanks @dom-christie!!! Quick question: does the solution change if we're not using jquery-ujs, but the new UJS in Rails 5.1 which uses vanilla javascript? (We still use jQuery in general, just not jquery-ujs) – Patrick O'Grady Oct 20 '17 at 10:04
  • 1
    @PatrickO'Grady it's mostly the same. Use `ajax:send` rather than `submit` (this may work with jquery-ujs too, but I have not tested it), and the disable/enable methods have changed to `Rails.disableElement`/`Rails.enableElement`. I have added a rails-ujs version above. – Dom Christie Oct 20 '17 at 10:44
  • @DomChristie now that this PR has been fixed, is this workaround still needed? https://github.com/rails/rails/pull/31441 – alexventuraio Feb 02 '21 at 18:05
3

FYI: it's a known issue by Rails "rails-ujs prematurely enables disabled elements for XHR requests with redirects".

And there's two PRs to fix it: #1 and #2.

Hopefully, in near future you won't need any workaround.

  • 1
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/low-quality-posts/18915701) – Thomas Smyth - Treliant Feb 23 '18 at 17:51
  • I understand and see your point. But the links contains code changes on the Turbolinks library only, there's nothing that the user of the library can do about them. Maybe this answer could be a comment on the first answer only, but unfortunately, I can't comment on Stack Overflow yet. – Gabril Lima Feb 27 '18 at 10:36
  • This was just fixed in Rails: https://github.com/rails/rails/pull/31441 and turbolinks-rails: https://github.com/turbolinks/turbolinks-rails/pull/28. Now we have to wait for Rails 6 and new release of turbolinks-rails :). – head Sep 28 '18 at 09:37
-1

how to submit form data and disable the button.

<form method="post" enctype="multipart/form-data" action="action.php">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title"> submit and disabled button </h3>
        </div>
        <div class="panel-body">
            {% csrf_token %}
            <div class="form-group">
                <label>Montant</label>
                <input type="number" name="montant" id="montant" required/>
            </div>
            <div class="form-group">
                <center><span id="msgError"></span></center>
            </div>
        </div>
    </div>
<div class="form-group">
    <button type="submit" name="save" id="save">Enregistrer</button>
</div>

</form>`

<script>

    $(document).ready(function(){
        var count = 0;
        $('#save').click(function(){
           var montant = $('#montant').val();
           if (montant!=''){
               count++;
               var montant = $('#montant').val();
               // first click data are sending
               if (count == 1){
                  return true;
               }else{
               // disable button 
                  $('#msgError').text('Merci de patienter...');
                  $('#msgError').css('color', 'blue');
                  $('#save').attr('disabled', 'disabled');
               }
           }
        });
   });
</script>