I'm working on a project using TypeScript and I'm facing an issue on elements having duplicate addEventListener to it.
The way the application works is that when the page first loads, nothing gets executed. All the TS classes keep alive waiting for an event to be dispatched. This event comes from the backend that will tell the application that it's ready to start.
const dbEvent = new CustomEvent("backendLoadEvent");
document.dispatchEvent(dbEvent);
When the TS class receives this event, it starts itself going through the dom getting the elements and applying all the eventListeners necessary for it to work properly.
Inside the TS file I will add the listener just as usual:
const btnOrder = document.getElementById('btn-order');
btnOrder.addEventListener('click', function (e) {
// Business logic here
console.log('debug button order click');
});
The problem is that the backend gets fired multiple times which is causing the button clicked to be executed multiple times as well. For example, sometimes that console.log inside the event click gets triggered twice simultaneously.
I tried to do something like this:
const btnOrder = document.getElementById('btn-order');
if (btnOrder.getAttribute('listener') !== 'true') {
btnOrder.addEventListener('click', function (e) {
const elementClicked = e.target;
elementClicked.setAttribute('listener', 'true');
});
}
The problem with this approach is that it relies on the click, so if the user clicks after the second event from the database, it's not going to work. I don't know when the event will arrive at the application nor when the user will click on the button.
Is there another approach I could use to prevent this behavior?
Note this is a simple example, but in some elements of the page there are a lot of events, this is a dashboard project. So we have like open/close modals, order and filter tables, add, remove and edit items on the table, and so on...
Related
I applied window scroll event and then I am getting elements with document.querySelectorAll. So, After executing this i have NodeList. I am looping over this nodelist with forEach.
In forEach i am triggering click event on item. But my click event trigger multiple time. How I can handle it.
Note:
Run this code on vimeo home page.
https://vimeo.com/home
window.addEventListener("scroll",()=>{
var config = document.querySelectorAll("[data-config-url]");
config.forEach((item)=>{
item.addEventListener("click",(e)=>{
var configData = e.currentTarget.getAttribute("data-config-url");
console.log(configData);
});
});
});
I want log configData of item for single time.
As stated on my comments above, I couldn't hit the scenario you described at the url https://vimeo.com/home. Not even when adopting the mobile screen simulation. I'm using Firefox 101.0.1 (64 bit) on Windows 10 in this moment.
document.querySelectorAll("[data-config-url]").length always returns zero and that's true also after trying to navigate the page, scrolling, opening menu blocks and so on. I didn't login because I don't have an account and you didn't specify how the page is supposed to be visited beyond the url itself.
Anyway when the scenario gets hit, that's the code implementing the logic I suggested in my comments. I just slightly modified your code so that the handler gets added only if the element wasn't already processed previously.
It then repeats the operation once every 500ms:
setInterval(scanPageAndAddHandlers, 500);
function scanPageAndAddHandlers(){
console.log('scanning...');
var config = document.querySelectorAll("[data-config-url]");
config.forEach((item)=>{
if (item.dataset.handlerWasAdded != 'true'){
//add the listener for the click event to the item element
item.addEventListener("click",(e)=>{
var configData = e.currentTarget.getAttribute("data-config-url");
console.log(configData);
});
//mark this item as processed
item.dataset.handlerWasAdded = 'true';
}
});
}
How do you selectize.addItem("value") without triggering item_add?
I have a tags field that’s dependent on another selection. I need to automatically fill in tags whenever that other selection is changed, and then I need to have a listener run code whenever the user adds or removes a tag. I can’t figure out how to add the tags programmatically without triggering the item_add event, but I don’t want it triggering before the user’s even touched the tags.
(There is addItem(…, silent), but unless I’m mistaken, that only stops it from triggering the change event.)
Sample code:
$('#input-tags').selectize({
onItemAdd: function() {
alert("This should only appear by user action");
}
});
$tags = $('#input-tags')[0].selectize;
$tags.addItem("awesome");
$tags.addItem("neat");
JSFiddle
One solution (and the only one I know of) is to remove the event listeners before the addItems and add them back afterwards. That hardly seems ideal, though, especially if it needs to be done more than once.
You can convert your code to register the add callback after adding all initial values:
$('#input-tags').selectize();
$tags = $('#input-tags')[0].selectize;
$tags.addItem("awesome");
$tags.addItem("neat");
// Register the listener once you have initialized the selectize component.
$tags.on('item_add', function() {
alert("This should only appear by user action");
});
I am having hard time while building e-commerce cart module with jquery.
Lets say that if i write a tags in html like this:
<div class="add-to-cart">+</div>
and then target it in my app:
this.$products,
this.$pa,
this.$ip,
this.$products = $('.shopperProducts'),
this.$pa = this.$products.find('.shopperAdd');
var self = this;
this.$ip = function() {
var init = function(action, product) {
/.../
};
self.$pa.on('click', function(event) {
event.preventDefault();
init('add', this);
});
};
This method is possible while im displaying products because they are displayed by php on page refresh so i have all the + links generated by php on html.
The problem is on the checkout file, this is the page when i display entire cart filled with products, cart must be generated and handled in jQuery and AJAX.
And code that i showed you doesnt work in cart page beacuse those links are appended for each product via jQuery into the DOM.
I have been study possible methods and there are few, the most in favour is to do this:
$(document).on('click', self.$pa, function(event) {
The problem with that solution is that it also is considered practice to be avoided due to high resources drain, i can see the difference in execution time myselfe, it takes a lot longer on low end devices. Is there some neat trick that can be used or method that is considered good practice to do in that situation?
<--- EDIT (Solution) --->
Instead of calling:
this.$products = $('.shopperProducts'),
this.$pa = this.$products.find('.shopperAdd');
on the beginning, i have to call it after i load elements into DOM and then they became targetable, then i just have to use self.$ip(); and event handlers can be attached. Without using any sort of workarounds, the solution was just to change order of executing commands.
There are two main strategies that you can use for adding click handlers for elements that you dynamically add to the dom.
One, You can add click handlers to the DOM element each time you create one
var addToCartButton = $('<div class="add-to-cart">+</div>');
addToCartButton.on('click', function(){
init('add', this);
};
// then you add your DOM element to the page
$('.container').append(addToCartButton);
Two, you can have a master click event listener on the page listen for all clicks where your buttons fall, and in your click handler, figure out whether the user is clicking on your element or not. This is ultimately more efficient and you don't have to add or remove event handlers each time you add elements to your page. This pattern is called event delegation, and here's another post on Stack that probably explains it better than I can
What is DOM Event delegation?
$('.container').click(function(event){
if ($(event.target).is('.add-to-cart') || $(event.target).parents().is('.add-to-cart')) {
// handle add to cart
}
})
BTW, your use of the self variable doesn't actually do anything, and neither does declaring this.$pa. You're basically accessing the property "$pa" of your this object, but not doing anything it.
Tried to bind submit event (or click or whatever) to an element within a jQuery mobile page. What I wanted to do is get the value from an input within an form element within a jQuery page and store it in cookies/localStorage. Every time I come back to this page I want to restore the input field.
Currently I ended up in using this script:
$('.pageClassSelector').live('pageinit', function (e) {
$('.classSelector').submit(function () {
var q = $('.inputClassSelector').val();
// store to localStorage ...
});
// load from localStorage ...
$('.q').val(lastSearchString);
});
This approach is documented there http://jquerymobile.com/test/docs/api/events.html
Since it seems possible that identical pages are hold in the DOM, ID selectors are not quite useful. My problem now is that everytime I navigate to this page the submit event is bound again and thus results in storing DIFFERENT (!) values. The submit event seems to be fired multiple times and much more interesting with the value before last.
Am I doing anything completly wrong? Any hints how to do scripting in jquery mobile properly?
TRY1:
I placed now the submit event binding within the pagebeforeshow event like so:
$('#PageId').on('pagebeforeshow', function (e) {
$('.classSelector').on('submit', function () {
var q = $('.q').val();
alert('stored: ' + q);
}
$('.q').val(lastSearchString);
}
But the value going to be stored is still the value before last, when I was navigating the page before. The first time it works as it should.
TRY2:
This is what I have now and it looks like it does that what I want. I select now the current page and select only the form which is a child of this page.
Is this good practice?
$('div:jqmData(id="PageId")').on('pagebeforeshow', function (e) {
$(this).find('#form').on('submit', function () {
var q = $(this).find('#input').val();
localStorage.setItem("key", q);
return true;
});
lastSearchString = localStorage.getItem("key");
$(this).find('#input').val(lastSearchString);
});
Your requirement to load from local storage and store on the page needs to be done by binding to the pagebeforeshow event (look at the section "Page transition events") and not the pageinit event like you are currently doing.
$('.pageClassSelector').on('pagebeforeshow', function (e) {
// load from localStorage ...
$('.q').val(lastSearchString);
});
Furthermore generally each page element (where you have data-role='page') should have a unique ID so you can use that instead of the CSS selector.
Multiple events firing when navigating pages sounds like multiple bindings to me, which is a known problem with jQuery Mobile. Bindings are not unbound when navigating pages, because everything is loaded through AJAX. See for example this StackOverflow Question: Jquery mobile .click firing multiple times on new page visit or my solution.
$('.classSelector').on('submit', function(){})
Try to use the constraction to bind your event to element.
Look likes some data was loaded through ajax request
I have a fairly large javascript class that generates an complete ajax-generated application. In one version of the ajax page there are a number of dropdown menus. These menus can get created and destroyed at various points during the life cycle of the application.
This is the behaviour I see:
User opens page version 1: no dropdowns
User goes to page version 2: dropdowns added with jQuery onchange event. Work as intended.
User returns to version 1 of page, dropdowns removed.
User returns to version 2 of page, dropdowns added again (using same element IDs)
dropdowns will now have 'double' event handling, triggering the event for each onchange.
The behaviour I'm struggling with is as follows.
On the initial page load, I add an onchange event:
function myClass(){
//Initiate once for current and future elements.
jQuery(document).on('change',".mydropdowns",
function(e){
self.submitDescriptionChange(this);
}
);
}
myClass.prototype.submitDescriptionChange = function (el){
doSomeAjaxStuff();
}
This works fine, except that each time the user goes to pages version 1 and returns to page version 2, the event gets multiplied. Very quickly you can end up with the event firing 20 times per change event, which in this case creates 20 ajax calls.
Logically, by using jQuery.off() I should be able to avoid this. But what happens instead is that the event is removed from both past and future elements, which means that when I recreate page version 2, the dropdowns won't work.
Every way I have tried this (and I've tried LOADS), I either end up with no event firing, or multiple events firing. I cannot seem to find a way to add/replace the elements whereby the event is only ever fired once.
Any ideas how I can solve this?
UPDATED
Yeah, so it turns out I misdiagnosed the problem. It actually came from repeatedly rebinding a 'hashchange' event, rather than rebinding the onchange event. Apologies for misdirection. Moving to bind() function to somewhere where it only executed once fixed the issue.
Since you do not want .off() to remove your events from other pages, I would suggest using namespaces for your events. For example, something like this:
function myClass(pageno) {
var pref_ev = 'mypage' + pageno + '.' + 'change';
$(document).off(pref_ev).on(pref_ev, ".mydropdowns", function(e) {
self.submitDescriptionChange(this);
});
}
This way, each page will have its own "change" event such as "mypage1.change". The event is still registered normally as a change event; the prefix namespace "mypage1" is used to only perform the .off() call on the right events.
I am not sure what plugin you are using for your dropdown menus but there should be a "destroy" method on that plugin. If you call that when removing the dropdowns that should work. Also, if you are only hiding the second page and not actually removing it from the DOM you dont have to re-invoke the plugin as the plugin will still be saved on the element.