Breako Breako
Breako Breako

Reputation: 6461

Preventing / dealing with double button clicks in angular

In angular we can set up a button to send ajax requests like this in view:

... ng-click="button-click"

and in controller:

...
$scope.buttonClicked = function() {
   ...
   ...
   // make ajax request 
   ...
   ...
}

So to prevent a double submit I could set a flag to buttonclicked = true when a button is click and unset it when the ajax callback is finished. But, even then control is handled back to angular who will updates to the Dom. That means there is a tiny window where the button could be clicked again before the original button click has completely 100% finished.

It's a small window but can still happen. Any tips to completely avoid this from happening - client side i.e. without making any updates to server.

Thanks

Upvotes: 42

Views: 100083

Answers (13)

Ganesh Giri
Ganesh Giri

Reputation: 1161

You can handle the form validation 
    $('form.your-form').validate({
        rules: {
            name: 'required',
            email: {
                required: true,
                email: true
            }
        },
        submitHandler: function (form) {
            // Prevent double submission
            if (!this.beenSubmitted) {
                this.beenSubmitted = true;
                form.submit();
            }
        }
    });

Upvotes: 0

Arashsoft
Arashsoft

Reputation: 2757

A simple solution I found and I think is better than other answer here is preventing browser default behavior on mousedown event.

ng-mousedown="$event.preventDefault();"

It does NOT prevent click event but it prevents double-click event :)

Upvotes: 0

Jonathan Palumbo
Jonathan Palumbo

Reputation: 6871

Using ng-disabled worked just fine in this example. No matter how furiously I clicked the console message only populated once.

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.submitData = function() {
    $scope.buttonDisabled = true;
    console.log("button clicked");
  }

  function augment() {
    var name, fn;
    for (name in $scope) {
      fn = $scope[name];
      if (typeof fn === 'function') {
        if (name.indexOf("$") !== -1) {
          $scope[name] = (function(name, fn) {
            var args = arguments;
            return function() {
              console.log("calling " + name);
              console.time(name);
              fn.apply(this, arguments);
              console.timeEnd(name);
            }
          })(name, fn);
        }
      }
    }
  }

  augment();
});
<!doctype html>
<html ng-app="plunker">

<head>
  <meta charset="utf-8">
  <title>AngularJS Plunker</title>
  <link rel="stylesheet" href="style.css">
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js"></script>
  <script src="app.js"></script>
</head>

<body ng-controller="MainCtrl">
  <input type="button" ng-click="submitData()" ng-disabled="buttonDisabled" value="Submit" />
</body>

</html>

I was curious exactly how long it takes for angular to apply the changes to the buttonDisabled flag. If you check the console in the plunker example it displays how long it takes the $eval and $apply methods to execute. On my machine it took an average of between 1-2 milliseconds.

Upvotes: 35

Ankit Pundhir
Ankit Pundhir

Reputation: 1095

You can create a directive to prevent double click:

angular.module('demo').directive('disableDoubleClick', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, elem, attrs) {
            elem.bind('click', function(){
                $timeout(function(){
                    elem.attr('disabled','disabled');
                }, 20);

                $timeout(function(){
                    elem.removeAttr('disabled');
                }, 500);
            });
        }
    };
});

and you can use it on any clickable item like this:

<button ng-click="clickMe()" disable-double-click >Click Me</button>

Upvotes: 1

Mati
Mati

Reputation: 1148

You can use a directive that I've just finished to prevent user from clicking multiple times on a element when performing an asynchronous action

https://github.com/mattiascaricato/angular-click-and-wait


You can add it into your project with npm or bower

npm install angular-click-and-wait

or

bower install angular-click-and-wait

Usage example

const myApp = angular.module('myApp', ['clickAndWait']);

myApp.controller('myCtrl', ($scope, $timeout) => {
  $scope.asyncAction = () => {
    // The following code simulates an async action
    return $timeout(() => angular.noop, 3000);
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<script src="https://cdn.rawgit.com/mattiascaricato/angular-click-and-wait/master/dist/click-and-wait.js"></script>

<section ng-app="myApp">
  <div ng-controller="myCtrl">
    <button click-and-wait="asyncAction()">Click Me!</button>
  </div>
</section>

Note: The asynchronous action passed as argument should be a Promise

Upvotes: 7

Daniel van Heerden
Daniel van Heerden

Reputation: 836

I recently had to do this, and I've brought a couple of solutions together. This works for me, it is a directive that is an alternative to ng-click that can only ever be clicked once.

This solution throws errors, which made it super easy to test.

.directive('oneClickOnly', [
    '$parse', '$compile', function($parse, $compile) {
        return {
            restrict: 'A',
            compile: function(tElement, tAttrs) {

                if (tAttrs.ngClick)
                    throw "Cannot have both ng-click and one-click-only on an element";

                tElement.attr('ng-click', 'oneClick($event)');
                tElement.attr('ng-dblclick', 'dblClickStopper($event)');

                tElement.removeAttr('one-click-only');
                var fn = $parse(tAttrs['oneClickOnly']);

                return {
                    pre: function(scope, iElement, iAttrs, controller) {
                        console.log(scope, controller);
                        var run = false;
                        scope.oneClick = function(event) {
                            if (run) {
                                throw "Already clicked";
                            }
                            run = true;
                            $(event.toElement).attr('disabled', 'disabled');

                            fn(scope, { $event: event });

                            return true;
                        };
                        scope.dblClickStopper = function(event) {
                            event.preventDefault();
                             throw "Double click not allowed!";
                            return false;
                        };

                        $compile(iElement)(scope);
                    }
                };
            },
            scope: true
        };
    }
])

Here are my tests (in case anybody is interested)

'use strict';
describe("The One click button directive", function() {
var $scope, testButton, $compile, clickedEvent;
var counter = 0;

beforeEach(function () {
    counter = 0;

    module('shared.form.validation');

    inject(function ($rootScope, _$compile_) {

        $compile = _$compile_;
        $scope = $rootScope.$new();

        $scope.clickEvent = function (event) {
            counter++;
        };
    });
});

it("prevents a button from being clicked multiple times", function () {

    var html = "<a one-click-only='clickEvent()'>test button</a>";
    testButton = $compile(html)($scope);

    $scope.$digest();

    testButton.click();
    expect(function () { testButton.click(); }).toThrow("Already clicked");

    expect(counter).toBe(1);
});

it("doesn't allow ng-click on the same tag", function() {
    var html = "<a ng-click='clickEvent()' one-click-only='clickEvent()'>test button</a>";
    expect(function () { $compile(html)($scope); }).toThrow("Cannot have both ng-click and one-click-only on an element");
});

it("works for multiple buttons on the same scope", function () {

    var counter2 = 0;
    $scope.clickEvent2 = function (event) {
        counter2++;
    };

    var html = "<a one-click-only='clickEvent()'>test button</a>";
    var html2 = "<a one-click-only='clickEvent2()'>test button</a>";
    testButton = $compile(html)($scope);
    var testButton2 = $compile(html2)($scope);

    $scope.$digest();

    testButton.click();
    expect(function () { testButton2.click(); }).not.toThrow("Already clicked");

    expect(counter).toBe(1);
    expect(counter2).toBe(1);
});
});

Upvotes: 2

Cedric
Cedric

Reputation: 5303

To expand again on zsong's doe :

First, with this solution, people can use double click to use your app. Old people sometime do that (as they were used to double click to open a program on windows), and other people do that by mistake too.

Second, the user can click as quickly as they can, their browser will wait for the server response before re-enabling the button (it is a fix for Mark Rajcok's comment on zsong's post : "If the AJAX request takes longer than the browser's double-click time/window, this won't work. I.e., a user could pause and click again.").

in your html

<ANY
  ng-click="buttonClicked();
            submitButtonDisabled = 1 + submitButtonDisabled;" 
  ng-disabled="submitButtonDisabled > 0;"
  ng-init="submitButtonDisabled = 0;"
>

in your controller

$scope.buttonClicked = function() {
    Service.doService.then(function(){
        //this is the callback for success
        // you probably do not want to renable the button here : the user has already sent the form once, that's it - but just if you want to :
        $scope.submitButtonDisabled --;
        //display a thank you message to the user instead
        //...
    }).error(function(){
        //this is the callback for the error
        $scope.submitButtonDisabled --;
    })
}

Upvotes: 0

Shreyas
Shreyas

Reputation: 1937

As suggested in one of the answers, I tried using ng-dbliclick="return false;" which gives JS warning.


Insted I used ng-dblclick="return" which is working smooth. Though this only works inside the <form> tag.

Upvotes: 0

Filipe Cotrim Melo
Filipe Cotrim Melo

Reputation: 121

I liked solution of user: zsong

But ng-dblclick="return false;" give a problem(I'm using Chrome Windows7) at js console you can see the error.

I can't comment (i don't have enough reputation to comment his solution)

Just use only ng-disabled.

As you can see at the plunker below if you have the two functions: ng-click and ng-dblclick And give a double click you will execute: 2 times click and 1 time dblclick

<bla ng-dblclick="count=count+1" ng-click="count=count+0.1" />

The double click gives you 1.2, so you can't prevent the 2 clicks with ng-dblclick, just add one more behavior when the second click happens.

Dblclick and click

Jonathan Palumbo gave an example with ng-disabled work at this thread.

Upvotes: 4

lucsorel
lucsorel

Reputation: 33

to elaborate on @Jonathan Palumbo's answer (use ngDisabled) and @andre's question ("how to use that in a directive instead of controller?"): to allow the click or submission event to bubble up, you need to set the 'disabled' attribute to your clickable element (be it a button, a link, a span or a div) programmatically inside a timeout function (even with a delay of 0ms) which allows the event to be passed before its being disabled:

$timeout(function(){ elm.attr('disabled',true); }, 0);

I refer to @arun-p-johny's answer: prevent multiple form submissions using angular.js - disable form button.

Upvotes: 1

zs2020
zs2020

Reputation: 54524

First you'd better add ngDblclick, when it detects the double click just return false:

<ANY ng-click="buttonClicked()" ng-dblclick="return false">

If you want to wait for the Ajax call to be finished, then you can disable the button by setting the ng-disabled

<ANY ng-click="buttonClicked()" ng-dblclick="return false;" ng-disabled="flag">

And in your controller, you can do

$scope.flag = false;
$scope.buttonClicked = function() {
    $scope.flag = true;
    Service.doService.then(function(){
        //this is the callback for success
        $scope.flag = false;
    }).error(function(){
        //this is the callback for the error
        $scope.flag = false;
    })
}

You need to handle both case when the ajax call is successfull or failed, since if it is failed, you don't want it show as diabled to confuse user.

Upvotes: 40

Angular Guy
Angular Guy

Reputation: 141

I just expanded on zsong's code to add a check in the handler for the flag. If its true then just return because a click is already being handled. This prevents double clicks without worrying about angular timing or that sort of thing.

$scope.flag = false;
$scope.buttonClicked = function() {
    if ($scope.flag) {
        return;
    }
    $scope.flag = true;
    Service.doService.then(function(){
        //this is the callback for success
        $scope.flag = false;
    }).error(function(){
        //this is the callback for the error
        $scope.flag = false;
    })
}

Upvotes: 14

Florian
Florian

Reputation: 3366

As suggested, using ng-disabled will solve your problem. I made a plunker to illustrate it here.

Upvotes: 1

Related Questions