Evitar el doble envío de formularios en jQuery

170

Tengo un formulario que el servidor tarda un poco en procesar. Necesito asegurarme de que el usuario espere y no intente volver a enviar el formulario haciendo clic nuevamente en el botón. Intenté usar el siguiente código jQuery:

<script type="text/javascript">
$(document).ready(function() {
    $("form#my_form").submit(function() {
        $('input').attr('disabled', 'disabled');
        $('a').attr('disabled', 'disabled');
        return true;
    });
});
</script>

Cuando intento esto en Firefox, todo se deshabilita, pero el formulario no se envía con ninguno de los datos POST que se supone que debe incluir. No puedo usar jQuery para enviar el formulario porque necesito que el botón se envíe con el formulario, ya que hay varios botones de envío y determino cuál fue utilizado por cuál valor se incluye en la POST. Necesito que el formulario se envíe tal como es y necesito deshabilitar todo inmediatamente después de que eso suceda.

¡Gracias!

Adán
fuente
¡Gracias a todos por toda su ayuda!
Adam

Respuestas:

315

Actualización en 2018 : acabo de recibir algunos puntos para esta vieja respuesta, y solo quería agregar que la mejor solución sería hacer que la operación sea idempotente para que los envíos duplicados sean inofensivos.

Por ejemplo, si el formulario crea un pedido, coloque una ID única en el formulario. La primera vez que el servidor ve una solicitud de creación de pedido con esa identificación, debe crearla y responder "éxito". Los envíos posteriores también deberían responder "éxito" (en caso de que el cliente no obtuviera la primera respuesta) pero no deberían cambiar nada.

Los duplicados deben detectarse mediante una comprobación de unicidad en la base de datos para evitar condiciones de carrera.


Creo que tu problema es esta línea:

$('input').attr('disabled','disabled');

Está deshabilitando TODAS las entradas, incluidas, supongo, aquellas cuyos datos se supone que debe enviar el formulario.

Para deshabilitar solo los botones de envío, puede hacer esto:

$('button[type=submit], input[type=submit]').prop('disabled',true);

Sin embargo, no creo que IE envíe el formulario si incluso esos botones están deshabilitados. Sugeriría un enfoque diferente.

Un complemento jQuery para resolverlo

Acabamos de resolver este problema con el siguiente código. El truco aquí es usar jQuery's data()para marcar el formulario como ya enviado o no. De esa manera, no tenemos que meternos con los botones de envío, lo que asusta a IE.

// jQuery plugin to prevent double submission of forms
jQuery.fn.preventDoubleSubmission = function() {
  $(this).on('submit',function(e){
    var $form = $(this);

    if ($form.data('submitted') === true) {
      // Previously submitted - don't submit again
      e.preventDefault();
    } else {
      // Mark it so that the next submit can be ignored
      $form.data('submitted', true);
    }
  });

  // Keep chainability
  return this;
};

Úselo así:

$('form').preventDoubleSubmission();

Si hay formas de AJAX que debe ser permitido enviar múltiples veces por carga de la página, puede darles una clase que indica que, a continuación, les excluyen de su selector de la siguiente manera:

$('form:not(.js-allow-double-submission)').preventDoubleSubmission();
Nathan Long
fuente
2
Sólo un puntero - ¿no sería mejor para almacenar en caché el $(this)que una var that = $(this)?
Stuart.Sklinar
55
@ Stuart.Sklinar - buena idea. Sin $formembargo, solía : 'formulario' porque es más descriptivo y el principal $porque, según mi convención, eso significa que es un objeto jQuery.
Nathan Long
21
Cuando use jquery, valide la validación discreta mejor para verificar si el formulario es válido. if ($ (this) .valid)) {$ form.data ('enviado', verdadero);}
cck
1
@cck Impresionante, lo usé. Tuviste un par de cierre adicional que eliminé: if ($ (this) .valid) {$ form.data ('submit', true);
Brian MacKay
1
@BrianMacKay si el formulario no es válido y se presiona el botón enviar, este complemento jquery marca el atributo enviado como verdadero, pero la validación discreta evita el envío debido a errores de validación. ¡Después de corregir los errores, uno no puede enviar el formulario porque el atributo enviado es verdadero!
cck
44

El enfoque de sincronización es incorrecto: ¿cómo sabe cuánto tiempo durará la acción en el navegador del cliente?

Cómo hacerlo

$('form').submit(function(){
  $(this).find(':submit').attr('disabled','disabled');
});

Cuando se envía el formulario, deshabilitará todos los botones de envío dentro.

Recuerde, en Firefox cuando deshabilita un botón, este estado se recordará cuando regrese al historial. Para evitar que tenga que habilitar botones en la carga de la página, por ejemplo.

Ctrl-C
fuente
2
+1 debido a la sugerencia de firefox, pero ¿hay alguna otra opción además de habilitar los botones en la carga de la página?
Raphael Isidro
1
Este es el camino más limpio. Estaba tratando de deshabilitar el botón Enviar con un controlador de eventos de clic, pero mi enfoque estaba causando problemas con Chrome. Esta solución funciona muy bien.
Anthony Hasta
1
Tenga mucho cuidado con este enfoque, si tiene un valor en el botón de envío y lo necesita, el formulario no lo enviará.
Fabien Ménager
De acuerdo con la última documentación de jQuery, debería usar .prop en lugar de .attr. Referencia: api.jquery.com/prop/#entry-longdesc-1 "El método .prop () debe usarse para establecer deshabilitado y marcado en lugar del método .attr ()".
J Plana
19

Creo que la respuesta de Nathan Long es el camino a seguir. Para mí, estoy usando la validación del lado del cliente, por lo que acabo de agregar una condición para que el formulario sea válido.

EDITAR : si esto no se agrega, el usuario nunca podrá enviar el formulario si la validación del lado del cliente encuentra un error.

        // jQuery plugin to prevent double submission of forms
        jQuery.fn.preventDoubleSubmission = function () {
            $(this).on('submit', function (e) {
                var $form = $(this);

                if ($form.data('submitted') === true) {
                    // Previously submitted - don't submit again
                    alert('Form already submitted. Please wait.');
                    e.preventDefault();
                } else {
                    // Mark it so that the next submit can be ignored
                    // ADDED requirement that form be valid
                    if($form.valid()) {
                        $form.data('submitted', true);
                    }
                }
            });

            // Keep chainability
            return this;
        };
PTK
fuente
9

event.timeStampNo funciona en Firefox. Devolver falso no es estándar, debe llamar event.preventDefault(). Y mientras lo hacemos, use siempre llaves con una construcción de control .

Para resumir todas las respuestas anteriores, aquí hay un complemento que hace el trabajo y funciona en varios navegadores.

jQuery.fn.preventDoubleSubmission = function() {

    var last_clicked, time_since_clicked;

    jQuery(this).bind('submit', function(event) {

        if(last_clicked) {
            time_since_clicked = jQuery.now() - last_clicked;
        }

        last_clicked = jQuery.now();

        if(time_since_clicked < 2000) {
            // Blocking form submit because it was too soon after the last submit.
            event.preventDefault();
        }

        return true;
    });
};

Para abordar Kern3l, el método de sincronización funciona para mí simplemente porque estamos tratando de detener un doble clic en el botón Enviar. Si tiene un tiempo de respuesta muy largo para un envío, le recomiendo reemplazar el botón o formulario de envío con una flecha giratoria.

El bloqueo completo de los envíos posteriores del formulario, como lo hacen la mayoría de los ejemplos anteriores, tiene un efecto secundario negativo: si hay una falla en la red y quieren intentar volver a enviarla, no podrían hacerlo y perderían los cambios que hecho. Esto definitivamente haría que un usuario enojado.

Robin Daugherty
fuente
2
Tienes razón. Puede ir al revés: deshabilitar instantáneamente y luego, después de un largo tiempo de espera, habilitar nuevamente. Evita los clics dobles, permite el reenvío, pero aún permite reenvíos no válidos para usuarios rezagados.
Ctrl-C
8

Por favor, consulte el complemento jquery-safeform .

Ejemplo de uso:

$('.safeform').safeform({
    timeout: 5000,  // disable next submission for 5 sec
    submit: function() {
        // You can put validation and ajax stuff here...

        // When done no need to wait for timeout, re-enable the form ASAP
        $(this).safeform('complete');
        return false;
    }
});
Max Kamenkov
fuente
4

... pero el formulario no se envía con ninguno de los datos POST que se supone que debe incluir.

Correcto. Los nombres / valores de elementos de formulario deshabilitados no se enviarán al servidor. Debe establecerlos como elementos de solo lectura .

Además, los anclajes no se pueden desactivar de esa manera. Deberá eliminar sus HREF (no recomendado) o evitar su comportamiento predeterminado (mejor manera), por ejemplo:

<script type="text/javascript">
$(document).ready(function(){
    $("form#my_form").submit(function(){
      $('input').attr('readonly', true);
      $('input[type=submit]').attr("disabled", "disabled");
      $('a').unbind("click").click(function(e) {
          e.preventDefault();
          // or return false;
      });
    });
</script>
karim79
fuente
@ Adam, sí, cualquier controlador de clic adjunto se disparará. ¿Pensé que solo querías evitar que fueran modificados?
karim79
@ karim79 No me importa si se activan los controladores de clics, solo quiero asegurarme de que el formulario no sea reenviado por el usuario que hace clic en otro botón de envío.
Adam
@ Adam - no, eso no sucederá. En mi ejemplo, agregué $('input[type=submit]').attr("disabled", "disabled");pero mi forma favorita es incluir onclick="this.disabled = 'disabled'"el onclick del envío.
karim79
@ karim79 Lo intenté, pero luego el botón Enviar que estoy haciendo clic no está incluido en la POST. Necesito que se incluya en la POST para determinar en qué botón se hizo clic
Adam
Sé que acabas de deshacer el evento click para 'a' pero por qué no solo usas $ ('a'). Click (function (e) {e.preventDefault (); // or return false;}); ?
Fabio
4

Existe la posibilidad de mejorar el enfoque de Nathan Long . Puede reemplazar la lógica para la detección del formulario ya enviado por este:

var lastTime = $(this).data("lastSubmitTime");
if (lastTime && typeof lastTime === "object") {
    var now = new Date();
    if ((now - lastTime) > 2000) // 2000ms
        return true;
    else
        return false;
}
$(this).data("lastSubmitTime", new Date());
return true; // or do an ajax call or smth else
Mikhail Fiadosenka
fuente
1
¿Por qué demonios usarías un temporizador para este enfoque?
Bobort
4

El código de Nathan pero para el complemento jQuery Validate

Si utiliza el complemento jQuery Validate, ya tienen implementado el controlador de envío, y en ese caso no hay razón para implementar más de uno. El código:

jQuery.validator.setDefaults({
  submitHandler: function(form){
    // Prevent double submit
    if($(form).data('submitted')===true){
      // Previously submitted - don't submit again
      return false;
    } else {
      // Mark form as 'submitted' so that the next submit can be ignored
      $(form).data('submitted', true);
      return true;
    }
  }
});

Puede expandirlo fácilmente dentro de } else { bloque para deshabilitar las entradas y / o enviar el botón.

Salud

Alph.Dev
fuente
2

Terminé usando ideas de esta publicación para llegar a una solución que es bastante similar a la versión de AtZako.

 jQuery.fn.preventDoubleSubmission = function() {

    var last_clicked, time_since_clicked;

    $(this).bind('submit', function(event){

    if(last_clicked) 
      time_since_clicked = event.timeStamp - last_clicked;

    last_clicked = event.timeStamp;

    if(time_since_clicked < 2000)
      return false;

    return true;
  });   
};

Usando así:

$('#my-form').preventDoubleSubmission();

Descubrí que las soluciones que no incluían algún tipo de tiempo de espera sino que simplemente desactivaban el envío o los elementos de formulario inhabilitados causaban problemas porque una vez que se activa el bloqueo, no puede enviar nuevamente hasta que actualice la página. Eso me causa algunos problemas cuando hago cosas de ajax.

Esto probablemente se puede mejorar un poco, ya que no es tan elegante.

jacklin
fuente
2

Si usa AJAX para publicar un formulario, el conjunto async: falsedebería evitar envíos adicionales antes de que se borre el formulario:

$("#form").submit(function(){
    var one = $("#one").val();
    var two = $("#two").val();
    $.ajax({
      type: "POST",
      async: false,  // <------ Will complete submit before allowing further action
      url: "process.php",
      data: "one="+one+"&two="+two+"&add=true",
      success: function(result){
        console.log(result);
        // do something with result
      },
      error: function(){alert('Error!')}
    });
    return false;
   }
});
Al Kari
fuente
3
Configurar <code> asnyc: true </code> es un mal enfoque porque congela el navegador hasta que se completa la solicitud, lo que supera la esencia de AJAX
Edwin M
2

La solución de Nathan modificada un poco para Bootstrap 3. Esto establecerá un texto de carga en el botón de envío. Además, expirará después de 30 segundos y permitirá que el formulario se vuelva a enviar.

jQuery.fn.preventDoubleSubmission = function() {
  $('input[type="submit"]').data('loading-text', 'Loading...');

  $(this).on('submit',function(e){
    var $form = $(this);

    $('input[type="submit"]', $form).button('loading');

    if ($form.data('submitted') === true) {
      // Previously submitted - don't submit again
      e.preventDefault();
    } else {
      // Mark it so that the next submit can be ignored
      $form.data('submitted', true);
      $form.setFormTimeout();
    }
  });

  // Keep chainability
  return this;
};

jQuery.fn.setFormTimeout = function() {
  var $form = $(this);
  setTimeout(function() {
    $('input[type="submit"]', $form).button('reset');
    alert('Form failed to submit within 30 seconds');
  }, 30000);
};
AlexZ
fuente
2

Use dos botones de envío.

<input id="sub" name="sub" type="submit" value="OK, Save">
<input id="sub2" name="sub2" type="submit" value="Hidden Submit" style="display:none">

Y jQuery:

$("#sub").click(function(){
  $(this).val("Please wait..");
  $(this).attr("disabled","disabled");
  $("#sub2").click();
});
izmir_LEE
fuente
2

Use un contador simple al enviar.

    var submitCounter = 0;
    function monitor() {
        submitCounter++;
        if (submitCounter < 2) {
            console.log('Submitted. Attempt: ' + submitCounter);
            return true;
        }
        console.log('Not Submitted. Attempt: ' + submitCounter);
        return false;
    }

Y llame a la monitor()función al enviar el formulario.

    <form action="/someAction.go" onsubmit="return monitor();" method="POST">
        ....
        <input type="submit" value="Save Data">
    </form>
Señor mak
fuente
1

He tenido problemas similares y mis soluciones son las siguientes.

Si no tiene ninguna validación del lado del cliente, simplemente puede usar el método jquery one () como se documenta aquí.

http://api.jquery.com/one/

Esto deshabilita el controlador después de haber sido invocado.

$("#mysavebuttonid").on("click", function () {
  $('form').submit();
});

Si está haciendo la validación del lado del cliente como lo estaba haciendo, entonces es un poco más complicado. El ejemplo anterior no le permitiría enviar nuevamente después de una validación fallida. Prueba este enfoque en su lugar

$("#mysavebuttonid").on("click", function (event) {
  $('form').submit();
  if (boolFormPassedClientSideValidation) {
        //form has passed client side validation and is going to be saved
        //now disable this button from future presses
        $(this).off(event);
   }
});
Mattbloke
fuente
1

Puede detener el segundo envío por este

$("form").submit(function() {
        // submit more than once return false
        $(this).submit(function() {
            return false;
        });
        // submit once return true
        return true; // or do what you want to do
    });
});
Mohammed Zayan
fuente
0

Mi solución:

// jQuery plugin to prevent double submission of forms
$.fn.preventDoubleSubmission = function () {
    var $form = $(this);

    $form.find('[type="submit"]').click(function () {
        $(this).prop('disabled', true);
        $form.submit();
    });

    // Keep chainability
    return this;
};

fuente
0

En mi caso, el formulario de envío del formulario tenía un código de validación, por lo que incrementé la respuesta de Nathan Long, incluido un punto de control de envío

$.fn.preventDoubleSubmission = function() {
      $(this).on('submit',function(e){
        var $form = $(this);
        //if the form has something in onsubmit
        var submitCode = $form.attr('onsubmit');
        if(submitCode != undefined && submitCode != ''){
            var submitFunction = new Function (submitCode);
            if(!submitFunction()){
                event.preventDefault();
                return false;
            }                   
        }

        if ($form.data('submitted') === true) {
            /*Previously submitted - don't submit again */
            e.preventDefault();
        } else {
          /*Mark it so that the next submit can be ignored*/
          $form.data('submitted', true);
        }
      });

      /*Keep chainability*/
      return this;
    };
bocapio
fuente
0

Cambiar el botón de enviar:

<input id="submitButtonId" type="submit" value="Delete" />

Con botón normal:

<input id="submitButtonId" type="button" value="Delete" />

Luego use la función de clic:

$("#submitButtonId").click(function () {
        $('#submitButtonId').prop('disabled', true);
        $('#myForm').submit();
    });

Y recuerde volver a habilitar el botón cuando sea necesario:

$('#submitButtonId').prop('disabled', false);
Dani
fuente
0

No puedo creer el buen truco css anticuado de los eventos de puntero: ninguno no ha sido mencionado todavía. Tuve el mismo problema al agregar un atributo deshabilitado, pero esto no se publica nuevamente. Pruebe lo siguiente y reemplace #SubmitButton con la ID de su botón de envío.

$(document).on('click', '#SubmitButton', function () {
    $(this).css('pointer-events', 'none');
})
Tom McDonough
fuente
Esto no detiene el envío de formularios cuando el usuario Enterpresiona la tecla.
reformado el
0

¿Por qué no solo esto? Esto enviará el formulario pero también deshabilitará el botón de envío,

   $('#myForm').on('submit', function(e) {
       var clickedSubmit = $(this).find('input[type=submit]:focus');
       $(clickedSubmit).prop('disabled', true);
   });

Además, si está utilizando jQuery Validate, puede colocar estas dos líneas debajo if ($('#myForm').valid()).

gen b.
fuente
0

este código mostrará la carga en la etiqueta del botón y establecerá el botón en

deshabilite el estado, luego, después del procesamiento, vuelva a habilitar y regrese el texto del botón original **

$(function () {

    $(".btn-Loading").each(function (idx, elm) {
        $(elm).click(function () {
            //do processing
            if ($(".input-validation-error").length > 0)
                return;
            $(this).attr("label", $(this).text()).text("loading ....");
            $(this).delay(1000).animate({ disabled: true }, 1000, function () {
                //original event call
                $.when($(elm).delay(1000).one("click")).done(function () {
                    $(this).animate({ disabled: false }, 1000, function () {
                        $(this).text($(this).attr("label"));
                    })
                });
                //processing finalized
            });
        });
    });
    // and fire it after definition
});
Mohamed.Abdo
fuente
-1

Resolví un problema muy similar usando:

$("#my_form").submit(function(){
    $('input[type=submit]').click(function(event){
        event.preventDefault();
    });
});
Nolan
fuente