yiisoft / yii-jquery

Yii Framework jQuery Extension
https://www.yiiframework.com/
BSD 3-Clause "New" or "Revised" License
27 stars 12 forks source link

New feature for client side prevention of unwanted multiple form submissions #17

Open dynasource opened 8 years ago

dynasource commented 8 years ago

As this is quite a hot topic (https://github.com/yiisoft/yii2/issues/10498#issuecomment-253542255, https://github.com/yiisoft/yii2/issues/12803 and more) its a good a idea not to wait too long with solutions. Many hours can be wasted to solve the problem and it is more complex than you would think at first hand.

The problem of this topic is the following:

There are many perspectives to tackle this problem from both serverside and clientside. From clientside, many people try to fix this with disabled buttons. However, disabling will not always prevent a submission. Multiple events can still be triggered, executing the submission anyway.

Another solution is a solution in which a 'ghost' button is used. This one is created on the fly and has the same CSS as its brother. This brother has no functionality and no events attached to it. You can click what you want but its just clicking on a 'useless' dummy. Depending on your configuration:

I have a first version of this solution ready. Its a draft which has not been in production yet. Mabye it already works perfectly, maybe it doesnt, so be advised. Of course, please share improvements.

$('.btn-primary').on('click', function (e) {
    var $button = $(this);
    if ($button.data('brother') == undefined) {
        var $brother = $(document.createElement($button[0].tagName));
        $brother.html('Please wait...');
        $brother.attr('disabled', true);
        $brother.addClass('disabled');
        $brother.addClass($button.attr('class'));
        $brother.hide();
        $brother.insertAfter($button);
        $button.data('brother', $brother)
    }else{
        var $brother = $button.data('brother');
    }

    if ($button.css('display') !== 'none') {
        $brother.show();
        $button.hide();
        setTimeout(function () {
            $brother.hide();
            $button.show();
        }, 1000);
    }
});

With respect to installing this script. You only need to have this one installed once so I have 1 asset (SubmitOnceAsset) implementing this script and this asset is loaded through a dependency in my app\common\Asset.php

cyphix333 commented 8 years ago

Haven't tried this yet, but pretty sure this:

$brother.insertAfter($button);

Should be:

$brother.after($button);

There is an insertAfter but it seems by your code you need the after version :)

Also, the value of $brother seems inconsistent...

Here:

var $brother = $(document.createElement($button[0].tagName));

You are setting it to an element, but here:

var $brother = $button.data('brother');

You are setting it to a string value.

Lastly, this bit:

$button.data('brother', $brother)

At this point, as noted above, $brother is set to an element, but you're setting the value of the data type there so it should be a string. :)

dynasource commented 8 years ago

Haven't tried this yet

You should. I am not posting a non-working version here ;)

cyphix333 commented 8 years ago

No, I meant I haven't tried the version you posted yet; just made my observations :)

wartur commented 8 years ago

Now I'm update code from #12814 using this solution (brother button)

This is my worked solution

    $('.jsBeforeSubmitFormBtn').click(function(){
        var $this = $(this);
        var $next = $this.next();
        if($next.hasClass('jsBeforeSubmitFormBtnBrother')) {
            $brotherBtn = $next;
        } else {
            $brotherBtn = $this.clone();
            $brotherBtn.attr('type', 'button');
            $brotherBtn.addClass('jsBeforeSubmitFormBtnBrother');
            $brotherBtn.removeClass('jsBeforeSubmitFormBtn');
            $brotherBtn.insertAfter($this);
            $brotherBtn.attr('disabled', 'disabled');
            $brotherBtn.prepend('<i class="glyphicon glyphicon-refresh"></i> ');
        }
        $this.hide();
        $brotherBtn.show();
    });
    $('.jsBeforeSubmitFormBtn').parents('form').on('afterValidate', function (event, messages, errorAttributes) {
        if(errorAttributes.length > 0) {
            $('.jsBeforeSubmitFormBtn').show();
            $('.jsBeforeSubmitFormBtnBrother').hide();
        }
    });

Spinner and some styles

.jsBeforeSubmitFormBtnBrother {
    cursor: progress;
}
.jsBeforeSubmitFormBtnBrother .glyphicon{
    -webkit-animation: glyphicon-spin 2s infinite linear;
    animation: glyphicon-spin 2s infinite linear;
}
dynasource commented 8 years ago

@wartur, thanks for sharing. Nice to see the similarities in this example.

One thing thats left to discuss is how to install this kind of script in an application.

We don't want to add .jsBeforeSubmitFormBtn to all of the buttons so you want a site-wide allocation with a generic CSS selector. IMO the most logical thing to put this locic is inside a SubmitOnceAsset with configuration options for the

wartur commented 8 years ago

@dynasource this is just by business solution. I do not think about deep architechure feaches. If this need to do as framework architecture solution, I think (not holywar) what moust usefull is use some js code attached via class (as I to do in my busintess sulution). That button need to have next feaches (via data attributes):

RUS: Это просто мое бизнес решение. Я особе не задумывался над некой архитектурой.

Если это делать как архитектурное решение, то мое мнение (не холиварю), что удобнее всего сделать такой функционал как я это сделал в моем бизнес решении - через подключаемый класс кнопки. Дополнительные же данные передавать через data-параметры, среди них:

muhammadcahya commented 8 years ago

If using jquery how about .one() click function ?

cyphix333 commented 8 years ago

@muhammadcahya I think that would only be useful if you were using jQuery to submit the form in the first place; on regular submissions it wouldn't do anything since you aren't binding any submit events to the button.

dynasource commented 8 years ago

perhabs its even better to make this a vanilla script. Not every app uses jquery.

beroso commented 8 years ago

Currently I'm using this approach on my projects:

// Handles beforeSubmit event (after all validations have passed)
jQuery('form.prevent-double-submit').on('beforeSubmit', function(event){
    if(jQuery(this).data('submitting')) {
        event.preventDefault();
        return false;
    }
    jQuery(this).data('submitting', true);
    return true;
});
PowerGamer1 commented 8 years ago

http://stackoverflow.com/a/926863

dynasource commented 7 years ago

just experienced that a jQuery version is also not to be preferable in high traffic websites in which jQuery is left out.