button click event fires multiple times inside of a Bootstrap 3 Modal - javascript

I have a bug (or feature) whereby any button that is inside a Bootstrap 3 modal will fire the click event several times. Besides going "old-school" and calling a script from an HTML button directly (which always works), is there a workaround for this?
<div class="modal-footer">
<span id="spanSendReport">
<button type="button" id="btnSendReport" class="btn btn-success" data-dismiss="modal">Send Report</button></span>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
And the jquery
$("#btnSendReport").click(function (e) {
... my code
});
Using Bootstrap v3.4.1 and jQuery v3.3.1
Never ran into this before (except with things posting back, but there are no postbacks of any kind happening - checked in the browser and in debugger).
There is only 1 button in the DOM with the Id (first thing I checked), but it fires the click event 4 times, every time!
Any ideas?
NOTE: I forgot to mention, the modal is opened from another modal. Maybe that has something to do with it.
The complete code inside the click event function (just in case it has something to do with it):
$("#btnSendReport").click(function (e) {
var parElem = $(this).parent().parent().parent();
var listID = parElem.find("#hidLID").val();
var problemTypes = $.map(parElem.find('option:selected'), function (e) { return e.value; }).join(',');
var problemTypeOther = parElem.find("#txtProblemTypeOther").val();
var obj = {};
obj.lid = listID;
obj.ProblemTypes = problemTypes;
obj.ProblemTypeOther = problemTypeOther;
try {
$.ajax({
type: "POST",
url: "../../api/reportdiscrepancy/",
data: obj,
dataType: "json",
success: function (data) {
var result = data;
},
error: function (err) {
console.log(err);
}
});
} catch (err) {
console.log(err);
}
});

Found the problem and the answer to many issues I've been running into before and the culprit is function pageLoad() which is what older ASP.Net webforms uses for client side actions.
I removed the '#btnSendReport' click to OUTSIDE of the pageLoad() function and it's only firing once now.
It looks like any MS-AJAX accumulates ALL event bindings (not actually firing them) within an UpdatePanel, then a final call to a click event (or whatever) will fire x number of times.
I was clued in as I made 3 changes to get to the modal and the last thing I did was click on the button (so it fired 4 times).
I believe it may have to do with the default setting of the UpdatePanel with ChildrenAsTriggers=True (unfortunately, I have to leave that on as other functionality breaks if set to false).

Related

Captcha/JQuery - Click simulation only works once

Hello fellow stack overflowers. I'm using JQuery to simulate a click on an invisible button that's linked with reCaptcha. Multiple buttons need to be attached to reCaptcha and you can only have one reCaptcha in each page. So, I created a callback function that detects which button is pressed. Here is how I did that:
HTML:
<div class="invisible">
<button class="g-recaptcha"
data-sitekey="..."
data-callback="captcha_callback"
data-badge="inline"
data-type="image"
id="btn_captcha"></button>
</div>
Click event + click simulation:
$("#review_send").on("click", function() {
alert("HI");
$("#btn_captcha").data("button-pressed", "review");
$("#btn_captcha").click();
$("#btn_captcha").blur();
});
reCaptcha callback:
var captcha_callback = function(response) {
if($("#btn_captcha").data("button-pressed") === "mail") {
alert($("#btn_captcha").data("button-pressed"));
send_contact_mail(response);
} else if($("#btn_captcha").data("button-pressed") === "review") {
alert($("#btn_captcha").data("button-pressed"));
send_review(response);
}
};
What happens is; when I click #review_send for the first time everything works: It first alerts "HI" and then "review".. But when I press #review_send a second time I only get the alert with "HI".
I have discovered that the click works again after waiting a while.
Is this something JQuery/Javascript related, has it something to do with reCaptcha or does reCaptcha have a timeout?
Thanks in advance!
Soo, I made this workaround because I thought that you can't have multiple recaptcha on the same page. However, I figured out that there is a way to have multiple of them.
You can render reCaptcha's and reset them (both with javascript):
var widget1 = grecaptcha.render("divID", {
siteKey: "...",
type: 'image',
callback: function(response) {
send_review(response);
}
});
Reset:
grecaptcha.reset(widget1);
I only have to check which button I pressed to render in the correct div and/or reset the correct reCaptcha.

Continuous execution of function?

I have a function that handles clicks when certain condition is met.
Scenario:
I want to close automatically the modal when click the Sign In button only if certain condition is met. And that condition is inside the $scope.empSignin().
So what I did according to some sources in the google, I create a function that will handle click event when the condition is valid.
html
//$scope.closeModal function
<button id="closeModal" type="button" class="close" data-dismiss="modal" ng-click="closeModal()">×</button>
//$scope.empSignin function
<input type="button" value="Sign In" ng-click="empSignin(signinInfo)">
this is the html file that contains a button that is automatically clicked in js function.
file.js
$scope.closeModal = function(){
console.log('entered');
$timeout(function(){
var el = document.getElementById('closeModal');
angular.element(el).trigger('click');
}, 0);
}
$scope.empSignin = function(signinInfo){
var data = {
username : signinInfo.username,
password : signinInfo.password
}
$http.post('server/emp_signin.php', data).then(function(res){
window.alert(res.data.message);
$scope.isMessage = res.data.message;
if($scope.isMessage == 'Successfully Signed In'){
console.log('true');
$scope.closeModal();
} else { console.log('false'); }
});
}
this js file contains function that handles click event.
It is working as I expected but when that event happened, in browsers console there is continuous execution (for what I know).
My question is what is happening there and why is that? And how I can solve that issue cause it is taking plenty of my device resources when I view in taskmanager.
Here is the screenshot of console:
Its because you are calling closeModal function on click event of the <button> and in the function, you are triggering click event again and which again calls closeModal function and thus it goes to infinite loop.And so you are seeing entered text in your console so many times. That's the issue.
angular.element(el).trigger('click'); // This is putting your code in infinite execution.
So remove above-mentioned line and there won't be the continuous execution of your code.

How to set ButtonState Loading in HTML5 CSHTML Page

Here is my HTML entry that fires the GenerateBill() Javascript at the moment :
<a class="btn btn-primary" id="loading-example-btn" data-loading-text="Loading..." onclick="GenerateBill()">Generate Bill</a>
Here is the GenerateBill() method, this all works fine, all I want to do is add the button state feedback
function GenerateBill() {
var url = '/PremiseProvider/GenerateBill';
var data = {
StartDate: $('#from').val(),
EndDate: $('#to').val(),
premiseProviderId: $('#PremiseProviderId').val()
};
$("body").load(url, data);
};
Here is a code snippet from the Bootstrap 3 official Site on how to implement the button state feedback:
<script>
$('#loading-example-btn').click(function () {
var btn = $(this)
btn.button('loading')
$.ajax(...).always(function () {
btn.button('reset')
});
});
</script>
My Question is how can I implement in my GenerateBill script, the bootstrap example uses an Ajax call, can I make it work without making too many changes to what I have?
If I might make a few suggestions that will both fix your issue and improve your code.
Instead of using an onclick event, add an event listener in your javascript, and call the function from there.
Add the .button('loading') call to that same event listener.
Don't leave off the href for an <a> tag. It will cause some browsers to not show the pointer correctly on hover.
Your link will look as follows:
Generate Bill
Leaving your GenerateBill() logic alone, the listener you need to add to your javascript:
$('#loading-example-btn').click(function () {
$(this).button('loading');
GenerateBill();
});
A working example of this code (with GenerateBill() simplified) is available here: http://www.bootply.com/VTSNA1XMcm

jQuery AJAX Request Repeating on Click event

Im building a small application and I have some click events binded to some span tags that trigger AJAX requests to a PHP file which queries a MySQL database and spits out the results to populate the targeted area.
However, sometimes i will be clicking the buttons and I have conditionals in place to stop multiple clicking to prevent duplicate content being added numerous times.
I click on a button and firebug tells me that the ajax request had actioned more than once, sometimes it will multiply - so it will start by doing it 2 times or another time it will carry our the request 8 times on one click and obviously flood my content area with duplicate data.
Any ideas?
EDIT
Code for a button is as follows:
<span class="btn"><b>Material</b></span>
This would be enabled by
$('.btn').bind('click', matOption);
and this would be controlled by something like this
var matOption = function() {
$(this).addClass('active');
// remove colours if change of mind on materials
if($('#selectedColour').val() >= 1) {
$('.colour').slideUp(500).children().remove();
$('#selectedColour').val('');
$('.matColOpt .btn').html('<b>Material Colour</b>').removeClass('active').css('opacity', 0.55);
$('.btn').eq(2).unbind('click', colOption); // add click to colour
$('#stage h1 span').eq(2).fadeOut(500);
$('.paperOpt .btn').css('opacity', 0.55).unbind('click', selectPaper);
}
// ajax req for available materials
var cid = $('#selectedColour').val();
var target = $('#notebookOpts .matOpt ul');
$.ajax({
type: "GET",
url: ajaxFile+"?method=getMaterials",
beforeSend: function() {if($('.mats').children('li').size() >= 1) { return false; }},
success: function(data) {
target.append(data).slideDown(500);
$('.mats li').bind('click', matSelect);
},
error: function() {alert('An unexpected error has occurred! Please try again.');}
});
};
You're probably binding your matOption function more than once.
if(!window.matOptionBound){
$('.btn').bind('click', matOption);
window.matOptionBound = true;
}
If you have a code that binds an event handler to a DOM element repeatedly then that event handler does gets executed repeatedly on the event. so if your code such
$("span").click(myHandlerFunction)
gets executed thrice, then you have just told jQuery to fire myHandlerFunction thrice on every click of span. It would be good to make sure there is no such condition goign on in your code. If that is not true then please post your code so that I can help further.
PS: The safest way to do this will be as
$("span").unbind("click",myHandlerFunction).bind("click",myHandlerFunction)

jQuery UI Dialog instead of alert() for Rails 3 data-confirm attribute

In Rails 3, passing a :confirm parameter to link_to will populate the data-confirm attribute of the link. This will induce a JS alert() when the link is clicked.
I am using the rails jQuery UJS adapter (https://github.com/rails/jquery-ujs). The relevant code from rails.js is:
$('body').delegate('a[data-confirm], button[data-confirm], input[data-confirm]', 'click.rails', function () {
var el = $(this);
if (el.triggerAndReturn('confirm')) {
if (!confirm(el.attr('data-confirm'))) {
return false;
}
}
});
and
triggerAndReturn: function (name, data) {
var event = new $.Event(name);
this.trigger(event, data);
return event.result !== false;
}
I would like to know how this could be modified to instead yield a jQuery dialog (e.g. the jQuery UI Dialog) allowing the user to confirm or cancel.
My knowledge of JavaScript isn't sufficient to achieve this elegantly. My current approach would be to simply rewrite the $('body').delegate() function to instead instantiate a lightbox. However I imagine that there is a more effective approach than this.
As others have mentioned, you cannot use a jQuery dialog box, as $.rails.confirm needs to block until it returns the users answer.
However, you can overwrite $.rails.allowAction in your application.js file like this:
$.rails.allowAction = function(element) {
var message = element.data('confirm'),
answer = false, callback;
if (!message) { return true; }
if ($.rails.fire(element, 'confirm')) {
myCustomConfirmBox(message, function() {
callback = $.rails.fire(element,
'confirm:complete', [answer]);
if(callback) {
var oldAllowAction = $.rails.allowAction;
$.rails.allowAction = function() { return true; };
element.trigger('click');
$.rails.allowAction = oldAllowAction;
}
});
}
return false;
}
function myCustomConfirmBox(message, callback) {
// implement your own confirm box here
// call callback() if the user says yes
}
It works by returning false immediately, thus effectively canceling the click event. However, your custom function can then call the callback to actually follow the link/submit the form.
I just added an external API to the Rails jquery-ujs for exactly this kind of customization. You can now make rails.js use a custom confirm dialog by plugging into (and re-writing 1 line of) the $.rails.allowAction function.
See my article, Rails jQuery UJS: Now Interactive, for a full explanation with examples.
EDIT: As of this commit, I moved the confirm dialog function to the $.rails object, so that it can be modified or swapped out even more easily now. E.g.
$.rails.confirm = function(message) { return myConfirmDialog(message); };
I liked the answer from #Marc Schütz about overriding $.rails.allowAction the most of anything I found online - but I'm not a big fan of overriding the functionality in allowAction since it's used all throughout the jquery-ujs codebase (what if there are side effects? Or if the source for that method changes in a future update?).
By far, the best approach would be to make $.rails.confirm return a promise... But it doesn't look like that's going to happen anytime soon :(
So... I rolled my own method which I think is worth mentioning because it's lighter weight than the method outlined above. It doesn't hijack allowAction. Here it is:
# Nuke the default confirmation dialog. Always return true
# since we don't want it blocking our custom modal.
$.rails.confirm = (message) -> true
# Hook into any data-confirm elements and pop a custom modal
$(document).on 'confirm', '[data-confirm]', ->
if !$(this).data('confirmed')
myCustomModal 'Are you sure?', $(this).data('confirm'), =>
$(this).data('confirmed', true)
$(this).trigger('click.rails')
false
else
true
# myCustomModal is a function that takes (title, message, confirmCallback)
How does it work? Well, if you look at the source, you'll notice that the allowAction method halts if the confirm event returns a falsy value. So the flow is:
User clicks link or button with data-confirm attribute. There is no data-confirmed present on the link or button, so we fall into the first if block, trigger our custom modal and return false, thereby stopping the action from continuing in the ujs click handler.
User confirms in the custom modal, and the callback is triggered. We store state on the element via data('confirmed', true) and re-trigger the same event that was triggered previously (click.rails).
This time the confirm event will fall into the else block (since data('confirmed') is truthy) and return true, causing the allowAction block to evaluate to true.
I'm sure I'm even missing other ways that might make this even simpler, but I think this is a really flexible approach to get a custom confirm modal without breaking core jquery-ujs functionality.
(Also, because we're using .on() this will bind to any data-confirm elements on the page at load time or in the future, similarly to how .delegate() works, in case you are wondering.)
I don't understand why you need to use the jQuery dialog when the JavaScript confirm() function will still work just fine. I would do something like this:
$('a[data-confirm]').click(funciton() {
confirm($(this).data("confirm"));
});
If you want to use a dialog instead, it's a little different. You can one-off each dialog you want, or you can probably take a uniform approach application wide so that your rails.js or your application.js can handle any dialog instance. For example, you'd need something like this on your page:
<a class="dialogLauncher">The link that creates your dialog</a>
<div class="dialog" title="My confirmation title" style="display:none">
<p>My confirmation message</p>
</div>
Then, in your js:
$('.dialogLauncher').click(function() {
var dialog = $(this).next('.dialog');
dialog.dialog();
})
If you want to customize your dialog a little more, check out this example.
Edit
Now that I think of it, this would be a good opportunity for a custom form builder. You could override one of your Rails link tags to output html similar to what's listed above whenever a certain attribute is present, i.e. :dialog => true. Surely that would be the Railsy way to do it. You could add other options into your tag as well, like the dialog title, etc.
Edit
Better yet, instead of :dialog => true, use :confirm => "my confirm message" just as you would normally, but in your override of link_to, you will use the :confirm option to create the dialog html that jQuery needs, delete that option, and then call super.
This is how I got it to work. Please suggest any corrections / improvements
#
in rails.js
#
// Added new variable
var deleteConfirmed = false;
// Changed function to use jquery dialog instead of confirm
$('body').delegate('a[data-confirm], button[data-confirm], input[data-confirm]', 'click.rails', function () {
var el = $(this);
/*
if (el.triggerAndReturn('confirm')) {
if (!confirm(el.attr('data-confirm'))) {
return false;
}
}
*/
if (el.triggerAndReturn('confirm')) {
if(deleteConfirmed) {
deleteConfirmed = false;
return true;
}
$( "#dialog-confirm" ).dialog("option", "buttons",
{
"Delete": function() {
$( this ).dialog( "close" );
deleteConfirmed = true;
el.trigger('click');
return true;
},
Cancel: function() {
$( this ).dialog( "close" );
return false;
}
}
);
$( "#dialog-confirm" ).dialog("open");
return false;
}
});
#
in application.js
#
//Ensure confirm Dialog is pre-created
jQuery(function () {
$( "#dialog-confirm" ).dialog({
autoOpen: false,
resizable: false,
height:140,
modal: true
});
});
#
in layout.html
Alt you can place this div anywhere in your generated html
#
<div id='dialog-confirm' title='Confirm Delete'>
<p>
<span class='ui-icon-alert' style='float:left; margin:0 7px 20px 0;'>
This item will be permanently deleted. Are you sure?
</span>
</p>
</div>
This is how I solved this problem.
I tried a lot of different ways, but only this one works.
In rails.js
function myCustomConfirmBox(element, callback) {
const modalConfirmDestroy = document.getElementById('modal-confirm');
// wire up cancel
$("#modal-confirm #cancel-delete").click(function (e) {
e.preventDefault();
modalConfirmDestroy.classList.remove('modal--open');
});
// wire up OK button.
$("#modal-confirm #confirm-delete").click(function (e) {
e.preventDefault();
modalConfirmDestroy.classList.remove('modal--open');
callback(element, true);
});
// show the dialog.
modalConfirmDestroy.classList.add('modal--open');
}
In this place I used code of #Mark G. with some changes. Because this $(this).trigger('click.rails') snipped of the code didn't work for me.
$.rails.confirm = function(message) {return true};
$(document).on('confirm', '[data-confirm]', (event)=> {
if (!$(this).data('confirmed'))
{
myCustomConfirmBox($(this), (element, choice)=> {
element.data('confirmed', choice);
let clickedElement = document.getElementById(event.target.id);
clickedElement.click();
});
return false;
}
else
{
return true;
}
});
Then in the html.erb file I have this code for link:
<%= link_to "documents/#{document.id}", method: "delete", data: {confirm: "sure?"}, id: "document_#{document.id}" %>
and this code for modal:
<div id="modal-confirm" class="modal modal-confirm">
<h2 class="modal__ttl">Title</h2>
<div class="modal__inner">
<p>Description</p>
<div class="modal__btns">
<button type="button" name="cancel" id="cancel-delete" class="btn btn-primary">Cancel</button>
<button type="button" name="confirm" id="confirm-delete" class="btn delete_button btn-secondary">Delete</button>
</div>
</div>
</div>
I hope, it will help someone.

Categories