0

I'm having some problems with users clicking buttons multiple times and I want to suppress/ignore clicks while the first Ajax request does its thing. For example if a user wants add items to their shopping cart, they click the add button. If they click the add button multiple times, it throws a PK violation because its trying to insert duplicate items into a cart.

So there are some possible solutions mentioned here: Prevent a double click on a button with knockout.js and here: How to prevent a double-click using jQuery?

However, I'm wondering if the approach below is another possible solution. Currently I use a transparent "Saving" div that covers the entire screen to try to prevent click throughs, but still some people manage to get a double click in. I'm assuming because they can click faster than the div can render. To combat this, I'm trying to put a lock on the Ajax call using a global variable.

The Button

 <a href="#" class="disabled btn btn-default" data-bind="click: $root.AddItemToCart, visible: InCart() == false"><span style="SomeStyles">Add</span></a>

Knockout executes this script on button click

 vmProductsIndex.AddItemToCart = function (item) {
      if (!app.ajaxService.inCriticalSection()) {
                    app.ajaxService.criticalSection(true);
                    app.ajaxService.ajaxPostJson("@Url.Action("AddItemToCart", "Products")",
                        ko.mapping.toJSON(item),
                        function (result) {
                            ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
                            item.InCart(true);
                            item.QuantityOriginal(item.Quantity());
                        },
                        function (result) {
                            $("#error-modal").modal();
                        },
                        vmProductsIndex.ModalErrors);
                    app.ajaxService.criticalSection(false);
                }
 }

That calls this script

(function (app) {
  "use strict";
  var criticalSectionInd = false;
  app.ajaxService = (function () {
      var ajaxPostJson = function (method, jsonIn, callback, errorCallback, errorArray) {
         //Add the item to the cart
        }
    };
    var inCriticalSection = function () {
        if (criticalSectionInd)
            return true;
        else
            return false;
    };

    var criticalSection = function (flag) {
        criticalSectionInd = flag;
    };
    // returns the app.ajaxService object with these functions defined
    return {
        ajaxPostJson: ajaxPostJson,
        ajaxGetJson: ajaxGetJson,
        setAntiForgeryTokenData: setAntiForgeryTokenData,
        inCriticalSection: inCriticalSection,
        criticalSection: criticalSection
    };
  })();
}(app));

The problem is still I can spam click the button and get the primary key violation. I don't know if this approach is just flawed and Knockout isn't quick enough to update the button's visible binding before the first Ajax call finishes or if every time they click the button a new instance of the criticalSectionInd is created and not truely acting as a global variable.

If I'm going about it wrong I'll use the approaches mentioned in the other posts, its just this approach seems simpler to implement without having to refactor all of my buttons to use the jQuery One() feature.

Community
  • 1
  • 1
Rafiki
  • 630
  • 1
  • 8
  • 22
  • 1
    maybe you could provide a basic jsbin ? your script looks ok, but it would be good to debug it and I would recommend to use debounce methods http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/ – Fer To Oct 14 '15 at 14:46

1 Answers1

1

You should set app.ajaxService.criticalSection(false); in the callback methods.

right now you are executing this line of code at the end of your if clause and not inside of the success or error callback, so it gets executed before your ajax call is finished.

vmProductsIndex.AddItemToCart = function (item) {
  if (!app.ajaxService.inCriticalSection()) {
    app.ajaxService.criticalSection(true);
    app.ajaxService.ajaxPostJson("@Url.Action("AddItemToCart", "Products")",
        ko.mapping.toJSON(item),
        function (result) {
            ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
            item.InCart(true);
            item.QuantityOriginal(item.Quantity());
            app.ajaxService.criticalSection(false);
        },
        function (result) {
            $("#error-modal").modal();
            app.ajaxService.criticalSection(false);
        },
        vmProductsIndex.ModalErrors);

    }
}

you could use the "disable" binding from knockout to prevent the click binding of the anchor tag to be fired.

here is a little snippet for that. just set a flag to true when your action starts and set it to false again when execution is finished. in the meantime, the disable binding prevents the user from executing the click function.

function viewModel(){
    var self = this;
    self.disableAnchor = ko.observable(false);
    self.randomList = ko.observableArray();
    self.loading = ko.observable(false);
    
    self.doWork = function(){
        if(self.loading()) return;
        
        self.loading(true);
        setTimeout(function(){
            self.randomList.push("Item " + (self.randomList().length + 1)); 
            self.loading(false);
        }, 1000);
    }
}

ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<a href="#" data-bind="disable: disableAnchor, click: doWork">Click me</a>

<br />

<div data-bind="visible: loading">...Loading...</div>

<br />
<div data-bind="foreach: randomList">
    <div data-bind="text: $data"></div>
</div>
chris vietor
  • 2,050
  • 1
  • 20
  • 29
  • This got it. I can't believe I didn't think about the script continuing execution while the Ajax call was being made. Good catch. Thank you. – Rafiki Oct 14 '15 at 17:52