How to prevent doubleclicking on a button with angularjs? - angularjs

I want to select an element in my case a button on the page in my angularcontroller and then disable it. The button looks like this:
myBtn= $element.by.buttonText('submit')
I don't want the user to click the button twice in order to avoid to post requests in the backend. When I get the code above I get an angular reference order. What is an easy way to select a button and then set the disabled property to true so the user cannot click the button twice?

You can use ng-disabled to disable your button according to a flag set in your submit function. For example:
<form ng-submit="submit()">
...
<button type="submit" ng-disabled="isSubmitting">Submit</button>
</form>
and in your controller:
$scope.submit = function() {
$scope.isSubmitting = true;
$http.post('...').finally(function() {
$scope.isSubmitting = false;
});
};

if you have many buttons on page, then its better to create a directive so that on any button which is clickable, it doesnt get pressed twice
app.directive('ngClickDisable', function() {
return {
scope: {
clickAndDisable: '&'
},
link: function(scope, iElement, iAttrs) {
iElement.bind('click', function() {
iElement.prop('disabled',true);
scope.clickAndDisable().finally(function() {
iElement.prop('disabled',false);
})
});
}
};
});
This can be used on a button as follows:
<button ng-click-disable="functionThatReturnsPromise()">Click me</button>

Related

How to make custom directive with dialog box template using Angular JS?

I need to show a confirmation box on different pages. So i have decided to create a custom directive for performing this task. I have a html template for confirmation box.There are two buttons and some text in this template. One button is for cancelling the dialog box and one for submitting it. So the functionality will be different for each page when we click on submit button. I have couple of questions regarding this issue.
How to create this kind of directive to show a dialog box on some condition?
How to pass text from my controller to this template?
How to override the "Submit" button functionality.
I had similar requirement where I wanted a custom modal pop-up to alert the user to continue with his actions such as delete, modify etc..,
So I wrote a custom directive. Below is the code.
(function(){
'use strict';
angular.module('mainApp').directive('confirm', ['$log','$uibModal', function($log,$uibModal){
var link = function($scope,elem,attr){
elem.bind('click',function(){
var modalInstance = $uibModal.open({
animation: true,
templateUrl: 'templates/shared/_confirm_modal.html',
controller: 'confirmDirectiveCtrl',
size: 'sm'
,backdrop: 'static' //disables modal closing by click on the backdrop.
,resolve: {
requiredVerbose: function(){
var requiredVerbose = {
modalTitle : attr.modalTitle
,message : attr.message
,confirmVerbose : attr.confirmVerbose
,cancelVerbose : attr.cancelVerbose
} ;
return requiredVerbose;
}
}
});
modalInstance.result.then(function(){
$scope.confirmFn();
}, function(){
if($scope.cancelFn){
$scope.cancelFn();
}
});
});
}
return{
restrict : 'A'
,scope : {
confirmFn : '&'
,cancelFn : '&'
}
,compile : function compile(elem,attr){
if(attr.confirmType && attr.confirmType=='delete')
{
attr.modalTitle = 'Warning';
attr.confirmVerbose = 'Delete';
attr.cancelVerbose = 'No';
attr.message = 'Are you sure, you want to delete?'
}
else{
if(!attr.modalTitle){attr.modalTitle = 'Warning'}
if(!attr.confirmVerbose){attr.confirmVerbose = 'Ok'}
if(!attr.cancelVerbose){attr.cancelVerbose = 'cancel'}
if(!attr.message){attr.message = 'Are you sure?'}
}
return{
post : link
}
}
}
}]);
angular.module('mainApp').controller('confirmDirectiveCtrl', ['$scope','$uibModalInstance','requiredVerbose',
function($scope,$uibModalInstance, requiredVerbose){
$scope.modalTitle= requiredVerbose.modalTitle;
$scope.message = requiredVerbose.message;
$scope.confirmVerbose = requiredVerbose.confirmVerbose;
$scope.cancelVerbose= requiredVerbose.cancelVerbose;
$scope.ok = function(){
$uibModalInstance.close($scope.timeline);
};
$scope.cancel = function(){
$uibModalInstance.dismiss();
};
}]);
}());
To answer your questions,
This is attribute type directive. And the element on which you add this directive tag is bound to onclick function which generates the required popup.
How to pass text?
You can pass the required text through attributes. I wanted this directive to work only for two kinds of alerts and hence had only two different sets of texts. If you want custom texts everytime, you can pass them to directive through attrs.
How to override the submit functionality?
You can pass your custom submit and cancel to this directive and bind them to the popup submit and cancel functions. The above code does the same.
Edit :
HTML template and explanation:
Below is an example describing on how you can use this directive.
<i class="fa fa-trash-o"
confirm
confirm-fn="deletePlaylist($index)"
confirm-type="delete">
</i>
The above template is an trash icon. The attributes are
directive name : confirm
confirm-fn : The function that should be called after user seleting ok/submit etc..,
confirm-type : This attribute defines what type of popup you want to show. In my case, I often use 'delete' type and hence wrote the required verbose related to it. By default, I already defined the verbose(title, message, ok-button, cancel-button).
If you want your custom messages add them in the attributes. Below is one such example.
<i class="fa fa-trash-o"
confirm
confirm-fn="doingGreatFn()"
cancel-fn="justFineFn()"
modal-title="My Modal"
message="How are you doing?"
confirm-verbose="Great"
cancel-verbose="Just Fine">
</i>
I hope, this helps
You can create a directive like below to handle both submit & cancel at any page for different functionalities in any controller. I've created an isolated scope directive but you can use change it according to your need by creating child scope scope : true; or bindToController:true (controller specific)
app.directive('confirm', ['$log', '$modal' ,'$parse','$timeout','factory', function($log, $modal,$parse,$timeout,factory) {
return {
restrict: 'E',
template:'<button type="button" class="btn form-btn" '+
'ng-click="openModal()" ng-disabled="disable" >'+
'{{buttonName}}</button>',
replace: true,
transclude: false,
scope: {
name :'=name', //can set button name ..basically u can send a text
disable :'=disable' //set as an attribute in HTML to disable button
},
link: function ($scope, element, attrs) {
$scope.buttonName = $scope.name;
$scope.openModal= function() {
$scope.modal = $modal.open({
templateUrl: 'customConfirmModal.html',
scope:$scope,
persist: true,
backdrop: 'static'
});
};
$scope.cancel = function(){
$scope.modal.dismiss();
};
$scope.submit= function(){
factory.customSubmitCall($scope);//call the factory method which will call different functions depending on the need..
};
}
Create a factory to contain different functions which can be called at any controller by injecting factory.
app.factory('factory', ['$http','$rootScope','$filter',function($http,$rootScope,$filter){
factory.customSubmitCall = function ($scope){
if($rootScope.page ==1){ //check on which page you are performing action
$scope.pageOneSubmit(); //page specific function in that controller..
}else{
$scope.submit();
}
};
return factory;
}]);
In your HTML
<confirm name="Confirm" disable="disable"> </confirm>

How to use a different combination of triggers for uib-popover?

The official documentation at :https://angular-ui.github.io/bootstrap/#/popover says that the following trigger combos can be passed as param to the popover-trigger attribute :
mouseenter: mouseleave
click: click
outsideClick: outsideClick
focus: blur
none
I want to use a combination of
mouseenter: outsideClick
How to achieve this without using the popover-is-open attribute?
You can't, the docs state
The outsideClick trigger will cause the popover to toggle on click, and hide when anything else is clicked.
"anything else" includes the element itself, so toggeling the element using outsideClick on or off and will interfere with the natural behavior of other triggers.
for example if state your triggers like so popover-trigger="mouseleave outsideClick"
, the trigger mouseleave will hide the popover instead of showing it if you have already clicked the element, otherwise it will just show it on leave. (plunk).
If you can hack it using popover-is-open then continue doing so, if it bothers you too much you can always request a feature.
popover-trigger="mouseenter outsideClick" for the uib-popover directive does not seem to work as one would think.
Initially, I thought it meant the following:
On mouse enter show the popover
On mouse leave hide the popover
On click keep popover open in an active state
On outside click close popover if it is in an active state
Since it does not I needed a manual approach, the following is stated in the documentation.
For any non-supported value, the trigger will be used to both show and hide the popover. Using the 'none' trigger will disable the internal trigger(s), one can then use the popover-is-open attribute exclusively to show and hide the popover.
So I created some HTML like:
<span class="glyphicon glyphicon-info-sign"
ng-class="{'text-primary' : isInfoPopoverClicked}"
ng-click="toggleInfoPopoverClicked()"
ng-mouseenter="enterInfoPopover()"
ng-mouseleave="leaveInfoPopover()"
custom-click-outside="closeInfoPopover()"
uib-popover-template="'info.html'"
popover-trigger="'none'"
popover-is-open="isInfoPopoverOpen()"
popover-placement="auto top"
popover-append-to-body="true" >
</span>
The JS in the controller:
// Toggle popover's clicked active state
$scope.toggleInfoPopoverClicked = function() {
$scope.isInfoPopoverClicked = !$scope.isInfoPopoverClicked;
};
// Close the popover, used for outside click and close action inside the template
$scope.closeInfoPopover = function() {
delete $scope.isInfoPopoverClicked;
};
// On mouse enter, show the popover
$scope.enterInfoPopover = function() {
$scope.isInfoPopoverMouseEnter = true;
};
// On mouse leave, close the popover.
// If clicked active state is false set to undefined.
// This supports when the user clicks the icon to close,
// that mouse enter does not immediately display the popover again.
$scope.leaveInfoPopover = function() {
$scope.isInfoPopoverMouseEnter = false;
if(false === $scope.isInfoPopoverClicked) {
delete $scope.isInfoPopoverClicked;
}
};
// Expression used in the popover-is-open attribute
$scope.isInfoPopoverOpen = function() {
if($scope.isInfoPopoverClicked) {
return true;
} else if(false === $scope.isInfoPopoverClicked){
return false;
}
return $scope.isInfoPopoverMouseEnter;
};
The template for the uib-popover-template I used:
<div custom-stop-event="click" class="pull-right">
<span ng-click="closeInfoPopover()" class="glyphicon glyphicon-remove"></span>
<section>{{info}}</section>
</div>
Now the trickier part was that this solution required me to create two more directives.
One to close the popover when clicking outside the element.
Another to stop the click event fired inside the pop-up. Preventing it from closing the popover.
The custom-click-outside directive:
angular.module('LSPApp').directive('customClickOutside', ['$document', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&customClickOutside'
},
link: function (scope, element) {
var handler = function (event) {
if (element !== event.target && !element[0].contains(event.target)) {
scope.$applyAsync(function () {
scope.clickOutside();
});
}
};
// Might not work on elements that stop events from bubbling up
$document.on('click', handler);
// Clean up event so it does not keep firing after leaving scope
scope.$on('$destroy', function() {
$document.off('click', handler);
});
}
};
}]);
The custom-stop-event directive called from the template's HTML:
angular.module('LSPApp').directive('stopEvent', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.on(attr.stopEvent, function (e) {
e.stopPropagation();
});
}
};
});
Hopefully, this helps someone, my final solution had all this encapsulated in it's own directive to promote reuse.

angular-modal-service set focus not working

I am using the angular modal service and I am trying to implement some keypress functionality for a smooth UX.
<button type="button" autofocus ng-click="close('Yes')" class="btn btn-primary" data-dismiss="modal">Yes</button>
The problem is that when the modal pops up, it doesn't have focus. Focus remains on the button clicked to activate the modal.
Is there some way to reset autofocus without reloading the page? Or is there some way to grab focus when the modal activates, but have it do so each time the modal opens?
I tried implementing the focus service as described in the answer to this post, but I couldn't get it to work with the modal.
Here is a plunker that demonstrates the behavior:
Plunker
Here is the working demo:
Plunker
I ended up finding this example:
Programmatically Setting Focus
It accomplishes it with a directive on whatever element you want to get focus:
app.directive('syncFocusWith', function($timeout, $rootScope) {
return {
restrict: 'A',
scope: {
focusValue: "=syncFocusWith"
},
link: function($scope, $element, attrs) {
$scope.$watch("focusValue", function(currentValue, previousValue) {
if (currentValue === true && !previousValue) {
$element[0].focus();
} else if (currentValue === false && previousValue) {
$element[0].blur();
}
})
}
}
});
Getting it to work for my modal was easy. I just added this little timeout function to the modal's controller, giving it a half a second to display the modal before trying to set the focus:
$timeout(function() {
$scope.isFocused = true;
},500)
and this attribute on the element we want to get focus:
sync-focus-with="isFocused"

Custom directive to detect out side the button click

I need to write a custom directive to close (or hide) the button when click out side the button (empty area of the DOM). In other words anywhere except within the button ? This behaviour should be applied only for this button when I apply the custom directive.Any guide would be highly appreciated.
<button class="btn btn-primary" ng-click="CreateUpdate()">Submit</button>
app.directive('MyDirective', function() {
//content
});
Within the button directive you can inject $document. Then wire up on click on the document.
<button class="myButton btn btn-primary" ng--lick="CreateUpdate()">Submit</button>
mainModule.directive("myButton", ['$document',
function ($document) {
return {
restrict: "E",
link: function (scope, element, attrs) {
$document.bind('click', function(event){
//Get the element clicked
var clickedElement = angular.element(event.target);
// If the clickedElement is not same as button (say, the clicked element class is different from button class) then close the button.
}
}
})

AngularJS directives: ng-click is not triggered after blur

DEMO
Consider the following example:
<input type="text" ng-model="client.phoneNumber" phone-number>
<button ng-click="doSomething()">Do Something</button>
.directive("phoneNumber", function($compile) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.mobileNumberIsValid = true;
var errorTemplate = "<span ng-show='!mobileNumberIsValid'>Error</span>";
element.after($compile(errorTemplate)(scope)).on('blur', function() {
scope.$apply(function() {
scope.mobileNumberIsValid = /^\d*$/.test(element.val());
});
});
}
};
});
Looking at the demo, if you add say 'a' at the end of the phone number, and click the button, doSomething() is not called. If you click the button again, then doSomething() is called.
Why doSomething() is not called for the first time? Any ideas how to fix this?
Note: It is important to keep the validation on blur.
Explain
Use click button, mousedown event is triggered on button element.
Input is on blur, blur callback triggered to validate input value.
If invalid, error span is displayed, pushing button tag down, thus cursor left button area. If user release mouse, mouseup event is not triggered. This acts like click on a button but move outside of it before releasing mouse to cancel the button click. This is the reason ng-click is not triggered. Because mouseup event is not triggered on button.
Solution
Use ng-pattern to dynamically validate the input value, and show/hide error span immediately according to ngModel.$invalid property.
Demo 1 http://jsbin.com/epEBECAy/14/edit
----- Update 1 -----
According to author's request, updated answer with another solution.
Demo 2 http://jsbin.com/epEBECAy/21/edit?html,js
HTML
<body ng-app="Demo" ng-controller="DemoCtrl as demoCtrl">
<pre>{{ client | json }}</pre>
<div id="wrapper">
<input type="text" phone-number
ng-model="client.phoneNumber"
ng-blur="demoCtrl.validateInput(client.phoneNumber)">
</div>
<button ng-mousedown="demoCtrl.pauseValidation()"
ng-mouseup="demoCtrl.resumeValidation()"
ng-click="doSomething()">Do Something</button>
</body>
Logic
I used ng-blur directive on input to trigger validation. If ng-mousedown is triggered before ng-blur, ng-blur callback will be deferred until ng-mouseup is fired. This is accomplished by utilizing $q service.
Here: http://jsbin.com/epEBECAy/25/edit
As explained by other answers, the button is moved by the appearance of the span before an onmouseup event on the button occurs, thus causing the issue you are experiencing. The easiest way to accomplish what you want is to style the form in such a way that the appearance of the span does not cause the button to move (this is a good idea in general from a UX perspective). You can do this by wrapping the input element in a div with a style of white-space:nowrap. As long as there is enough horizontal space for the span, the button will not move and the ng-click event will work as expected.
<div id="wrapper">
<div style='white-space:nowrap;'>
<input type="text" ng-model="client.phoneNumber" phone-number>
</div>
<button ng-click="doSomething()">Do Something</button>
</div>
It is because the directive is inserting the <span>Error</span> underneath where the button is currently placed, interfering with the click event location. You can see this by moving the button above the text box, and everything should work fine.
EDIT:
If you really must have the error in the same position, and solve the issue without creating your own click directive, you can use ng-mousedown instead of ng-click. This will trigger the click code before handling the blur event.
Not a direct answer, but a suggestion for writing the directive differently (the html is the same):
http://jsbin.com/OTELeFe/1/
angular.module("Demo", [])
.controller("DemoCtrl", function($scope) {
$scope.client = {
phoneNumber: '0451785986'
};
$scope.doSomething = function() {
console.log('Doing...');
};
})
.directive("phoneNumber", function($compile) {
var errorTemplate = "<span ng-show='!mobileNumberIsValid'> Error </span>";
var link = function(scope, element, attrs) {
$compile(element.find("span"))(scope);
scope.mobileNumberIsValid = true;
scope.$watch('ngModel', function(v){
scope.mobileNumberIsValid = /^\d*$/.test(v);
});
};
var compile = function(element, attrs){
var h = element[0].outerHTML;
var newHtml = [
'<div>',
h.replace('phone-number', ''),
errorTemplate,
'</div>'
].join("\n");
element.replaceWith(newHtml);
return link;
};
return {
scope: {
ngModel: '='
},
compile: compile
};
});
I would suggest using $parsers and $setValidity way while validating phone number.
app.directive('phoneNumber', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ctrl) {
if (!ctrl) return;
ctrl.$parsers.unshift(function(viewValue) {
var valid = /^\d*$/.test(viewValue);
ctrl.$setValidity('phoneNumber', valid);
return viewValue;
});
ctrl.$formatters.unshift(function(modelValue) {
var valid = /^\d*$/.test(modelValue);
ctrl.$setValidity('phoneNumber', valid);
return modelValue;
});
}
}
});
So, you will be able to use $valid property on a field in your view:
<form name="form" ng-submit="doSomething()" novalidate>
<input type="text" name="phone" ng-model="phoneNumber" phone-number/>
<p ng-show="form.phone.$invalid">(Show on error)Wrong phone number</p>
</form>
If you want to show errors only on blur you can use (found here: AngularJS Forms - Validate Fields After User Has Left Field):
var focusDirective = function () {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attrs, ctrl) {
var elm = $(element);
if (!ctrl) return;
elm.on('focus', function () {
elm.addClass('has-focus');
ctrl.$hasFocus = true;
if(!scope.$$phase) scope.$digest();
});
elm.on('blur', function () {
elm.removeClass('has-focus');
elm.addClass('has-visited');
ctrl.$hasFocus = false;
ctrl.$hasVisited = true;
if(!scope.$$phase) scope.$digest();
});
elm.closest('form').on('submit', function () {
elm.addClass('has-visited');
ctrl.$hasFocus = false;
ctrl.$hasVisited = true;
if(!scope.$$phase) scope.$digest();
})
}
}
};
app.directive('input', focusDirective);
So, you will have hasFocus property if field is focused now and hasVisited property if that field blured one or more times:
<form name="form" ng-submit="doSomething()" novalidate>
<input type="text" name="phone" ng-model="phoneNumber" phone-number/>
<p ng-show="form.phone.$invalid">[error] Wrong phone number</p>
<p ng-show="form.phone.$invalid
&& form.phone.$hasVisited">[error && visited] Wrong phone number</p>
<p ng-show="form.phone.$invalid
&& !form.phone.$hasFocus">[error && blur] Wrong phone number</p>
<div><input type="submit" value="Submit"/></div>
</form>
Demo: http://jsfiddle.net/zVpWh/4/
I fixed it with the following.
<button class="submitButton form-control" type="submit" ng-mousedown="this.form.submit();" >

Resources