Reputation: 6461
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
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
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
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
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
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
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
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
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
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
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.
Jonathan Palumbo gave an example with ng-disabled work at this thread.
Upvotes: 4
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
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
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