JS function setting too many addEventListeners - javascript

I have a single page app (SPA) with search functionality where I am trying to track how users interact with the search bar in Adobe Analytics. If I navigate to 5 different "pages" of my SPA, the following code will fire the event 6 times. Seems the addEventListener is being set but not cleared if no search happens on a "page". These eventListeners essentially get queued each time a "page" loads, and once a search is entered, they all fire at the same time, clearing the queue.
How can I clear the eventListeners if a search is NOT performed and only have this event fire once a search is performed via pressing enter?
Suppose I have the code:
clearInterval(interval);
var interval = setInterval(function(){
if (document.querySelector("form div[data-autoid='search_bar'] input")) {
inputField = document.querySelector("form div[data-autoid='search_bar'] input")
inputField.addEventListener('keydown', searchEntered)
clearInterval(interval);
}
}, 500);
function searchEntered(e) {
if (e.key == "Enter") {
console.log("search entered");
var event = new CustomEvent('searchEntered');
dispatchEvent(event);
}
}

If you want remove an existing event listener, use EventTarget.removeEventListener().
As the MDN page states, you need to specify the same type and listener parameters that you passed to addEventListener(). If you passed options, also, you should also pass the same ones to removeEventListener():
Given an event listener previously added by calling addEventListener(), you may eventually come to a point at which you need to remove it. Obviously, you need to specify the same type and listener parameters to removeEventListener(), but what about the options or useCapture parameters?
While addEventListener() will let you add the same listener more than once for the same type if the options are different, the only option removeEventListener() checks is the capture/useCapture flag. Its value must match for removeEventListener() to match, but the other values don't.
Note that is not possible to add multiple identical listeners to the same element. From MDN page:
If multiple identical EventListeners are registered on the same EventTarget with the same parameters, the duplicate instances are discarded. They do not cause the EventListener to be called twice, and they do not need to be removed manually with the removeEventListener() method.

If you add the event listener to window it will capture all keydown events on the page (delegate target) once that happens make a check to see if it was triggered by the Enter key and if the parentNode has the attribute data-autoid='search_bar'.
If true dispatch your custom event.
This will continue to work even if the DOM changes
addEventListener('keydown', e => {
if (e.key == 'Enter' && e.target.parentNode.dataset.autoid == 'search_bar') {
console.log('search entered');
const event = new CustomEvent('searchEntered');
dispatchEvent(event);
}
})
<div data-autoid='search_bar'>
Will trigger <input />
</div>
<label>
Won't trigger <input />
</label>

var interval = setInterval(function(){
var inputField = document.querySelector("form div[data-autoid='search_bar'] input");
if (inputField && !inputField.dataset.searchEventAttached) {
inputField.addEventListener('keydown', searchEntered);
inputField.dataset.searchEventAttached = true;
}
clearInterval(interval);
}, 500);

Related

Why is the window.eventListener not being removed in my React code, although I am calling it?

I am working on the following code:
const [userNick, setUserNick] = useState("");
const [eventListenerActive, setEventListenerActive] = useState(false);
const promptOnLeaving = (ev) => {
ev.returnValue = true;
}
useEffect(() => {
if(userNick !== "" && !eventListenerActive){
console.log("ADDING EVENT LISTENER")
window.addEventListener('beforeunload', promptOnLeaving)
setEventListenerActive(true);
}
else if(userNick === "" && eventListenerActive){
console.log("REMOVING EVENT LISTENER")
window.removeEventListener('beforeunload', promptOnLeaving)
setEventListenerActive(false)
}
}, [userNick])
I want to add an event listener to my window, that listens on the beforeunloading-event, e.g the side will be closed, redirected or reloaded.
Since there are multiple input fields in the UI, I want to ask the user if he is sure he wants to close the page, although there are some form fields open.
One of them is the user nickname (here userNick).
When the user nick is empty, I want to remove the event listener, since there isn't any data that would be lost on a reload, so I, therefore, don't need to prompt the user.
On the other hand, if the user has entered text inside the UserNick input field, I want to add the event listener and prompt him, if he is sure he wants to close the page.
My problem here is, that the event listener doesn't get removed when the user empties the input field.
Adding the listener works without a problem and the listener itself does what it should.
But I can't seem to remove the listener properly.
When the else if block gets executed, my eventListenerActive state gets reset, and on the next user input there will be another eventListener making it 2 already.
I tried to add and remove the evenListeners through external buttons with the onClick-event, and that did actually work.
Additionally, I also read that the event-handling function should not be anonymous, when I want to remove it, so I gave it a name here to.
So what am I doing wrong here, that the eventListener won't be removed?
Thanks in advance
I figured it out myself.
This article was very helpful:
Removing event handlers in React
The actual problem was, that javascript internally compares the handler function when the listener was added with the handler function when the listener is being removed.
Since React re-renders the component, the handler function gets initialized again which results in an unmatch between the "old" handler function and the "new" one.
I had to add a useCallback-function to my handler, so the handler get memoized and stays the same when compared on removeEventListener:
const promptOnLeaving = useCallback((ev) => {
ev.returnValue = true;
}, [])

Replace/Remove EventListener from Buttons

I am attempting to remove an event listener from some buttons and it seems that my code was not doing anything. I have a map that has icons for places that have restaurants. Each icon has an event listener that triggers a function showInfo:
let showInfo = function(target, locationInfo) {
// handler
let toggleBtn = function(event) {
if (event.target.id === 'covidClosedBtn') {
openBtn.classList.remove('active');
closedBtn.classList.add('active');
} else if (event.target.id === 'covidOpenBtn') {
closedBtn.classList.remove('active');
openBtn.classList.add('active');
}
}
// add event listener to the closedBtn
if (closedBtn.getAttribute('listener') !== 'true') {
closedBtn.setAttribute('listener', 'true');
closedBtn.addEventListener('click', toggleBtn, false);
} else if (closedBtn.getAttribute('listener') === 'true') {
console.log('I want to remove this listener');
// this removeEventListener line does nothing
closedBtn.removeEventListener('click', toggleBtn, false);
}
When showInfo is called I add event listeners to both the open and closed button but to keep it simple I am focusing on the closed button to illustrate my difficulty.
The first time I click on an icon these buttons will have an attribute of listener added to indicate a listener exists. Any subsequent clicks should replace/remove the event listener and add a new one. The reason I need to do this is because I want the locationInfo for the icon to be available each time a different icon is clicked so I can further manipulate that information within the showInfo closure.
However, in my case the listener doesn't get removed. I have attempted to create a clone of the button and replace the old button with cloneNode but this causes the closedBtn variable to lose reference and I can no longer attach any listeners to it or manipulate its classes later on in my method.
I believe my handler in the remove method is not strictly equal to what it was when it was added but I cannot think of a solution for my scenario. How can I successfully remove/replace this event listener?
Do I understand correctly that you're calling showInfo twice, once to add and once to remove the listener?
If that's the case, then yes, the two closures toggleBtn are distinct. If you wish to make sure that you only have one toggleBtn, you must keep it outside of showInfo.

How do I disable and then re-enable a Hammer Event properly using Hammer.js?

I'm using hammer.js and jquery.hammer.js in order to handle multiple different types of events (mostly tap events).
I made a wrapper function to be used for any/all tap event listener declarations.
This is the function.
var OnClick = function(button, CallbackFunction, TurnBackOnAfterStartCallback)
{
if(TurnBackOnAfterStartCallback != false)
{
TurnBackOnAfterStartCallback = true;
}
if(!button)
{
LogResult("Error: Attempted to create Hammer Click Event Listener without assigning a jQuery Object to listen too...");
return;
}
if(!CallbackFunction)
{
LogResult("Error: Attempted to create Hammer Click Event Listener without assigning a Callback Function...");
return;
}
$(button).hammer().on("tap", function(event)
{
var target = event.target;
// Disable the button so that we can't spam the event....
$(target).hammer().off("tap");
// We receive the event Object, incase we need it...
// Then we call our CallBackFunction...
if(CallbackFunction)
{
CallbackFunction(target);
}
// Renable the button for future use if need be.
if(TurnBackOnAfterStartCallback)
{
$(target).hammer().on("tap", CallbackFunction);
}
});
};
When I register an event using this function it works as expected. First it disables the event listener so you can't spam the event by clicking the button 100 times... Like so...
$(target).hammer().off("tap");
Then it preforms any callback functionality if there exists any...
if(CallbackFunction)
{
CallbackFunction(target);
}
Finally we re-enable the button for future use, unless we've specified that it will not be turned back on...
// Renable the button for future use if need be.
if(TurnBackOnAfterStartCallback)
{
$(target).hammer().on("tap", CallbackFunction);
}
This works perfectly during the first event launch... However, once I trigger the event again the Callback function is sent the event and not the event.target for some reason...
If I remove the .off and .on calls then it works as expected but can be spammed...
For a live example checkout this jsfiddle... It prints the result to the console... The first output is correct, everything after that isn't as expected.
https://jsfiddle.net/xupd7nL1/12/
Never mind, I had a dumb moment there...
The issue was that I was calling the event listener directly and not through my wrapper function, OnClick...
In other words change...
if(TurnBackOnAfterStartCallback)
{
$(target).hammer().on("tap", CallbackFunction);
}
to
if(TurnBackOnAfterStartCallback)
{
OnClick(target, CallbackFunction, TurnBackOnAfterStartCallback);
}

jQuery - Event delegation is occuring multiple times

I'm using event delegation in the pagination for my website. When you click the < or > buttons it moves the page up or down. The problem is that if you don't release the mouse button, in a split-second it will keep repeating the click handler.
How can I make it so that this event only occurs once per-click? Here's my code:
$(document).on('mousedown', '#prev', function(event) {
// Page is the value of the URL parameter 'page'
if (page != 1) {
page--;
var state = {
"thisIsOnPopState": true
};
history.replaceState(state, "a", "?page=" + page + "");
}
// Refresh is a custom function that loads new items on the page
refresh();
});
You should use "click" event instead of "mousedown" unless you have a unavoidable reason.
But "mousedown" or "touchstart" event occurs when a user start pressing the mouse button or screen and it will not be fired until you release the button and press it again.
So I assume you are using a chattering mouse or mouses which has macro software.
change event into "click" and see if it works and in the case "click" event is not gonna solve the issue,try using another mouse.
FYI,underscore methods _.throttle or _.debounce might help to support chattering mouses.
throttle_.throttle(function, wait, [options])
Creates and returns a new, throttled version of the passed function, that, when invoked repeatedly, will only actually call the original function at most once per every wait milliseconds. Useful for rate-limiting events that occur faster than you can keep up with.
debounce_.debounce(function, wait, [immediate])
Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked. Useful for implementing behavior that should only happen after the input has stopped arriving. For example: rendering a preview of a Markdown comment, recalculating a layout after the window has stopped being resized, and so on.
http://underscorejs.org/
If you want to use a "delegated" event handler rather than a "direct" event handler to bubble up the event, try to use a more specific target selector than $(document) like $('.some-class') where some-class is the class name directly above the #prev element.
I would also use either the mouseup or click events instead to avoid the mousedown event firing while the mouse click is held down.
According to the API:
The majority of browser events bubble, or propagate, from the deepest,
innermost element (the event target) in the document where they occur
all the way up to the body and the document element.
Try this:
// delegated "click" listener using a more specific target selector
$('.some-class').on('click', '#prev', function(event) {})
You may want to check your HTML to see if you are using #prev multiple times. Usually, just creating the listener on the target ID element should work fine.
// direct "click" listener on an ID element
$('#prev').on('click', function(event) {})
I haven't found the answer to this question, but I have found a solution that fixes the problem. What I have done is added a conditional that only allows the click event to occur once-per-click:
var i = 0;
$(document).on('click', '#prev', function(event) {
if (page != 1 && i === 0) {
page--;
var state = {
"thisIsOnPopState": true
};
history.replaceState(state, "a", "?page=" + page + "");
i = 1;
refresh();
}
});
// Resets 'i' for the next click
$(document).on('mouseup', function() {
i = 0;
});

Multiple JS event handlers on single element

I am working with an existing web app, in the app there are a variety of submit buttons on different forms, some using regular http post, some defining an onClick function, and some binding a js event handler to the button using a class on the element.
What I want to do, is bind another event handler to these buttons by just adding a class to the buttons, but what I want to determine is will the new event handler be guaranteed to be executed, or could one of the form submit actions happen before it does meaning my new function isn't hit.
The example scenario is I want to add a class to these buttons that bimds them all to a common js function that simply logs usage to some api. Is there a risk that the logging function isn't called because the form submit has navigated away from the page?
I've not done loads of js development, and I could test this 100 times over and just get lucky with it firing.
Below is some code I have tested with for one of the examples - again, I'm not asking how to bind multiple events, the question is to about my understanding of the spec and whether execution of all handlers is guaranteed.
$(document).ready(function(){
$('.testingBtn').click(function() {
window.location.replace("http://stackoverflow.com");
});
$( ".testingBtn" ).click(function(){
alert('submitting!');
});
});
<input class="testingBtn" type="submit" id="submitform" value="Complete Signup" />
As seen above, I can bind the multiple events, and in this example, just directed to another url, but this could be a form.submit() etc. In my testing the alert has always fired first, but am I just getting lucky with the race conditions?
In JS, you don't really have control over what order the event handlers are called, but with careful delegation and well-placed listeners, it is possible.
Delegation is one of the most powerful features of the event model. As you may or may not know: in JS, an event is handed to the top of the dom, from where it propagates down to the element onto which the event should be applied. It stands to reason, therefore, that an event listener attached to the global object will call its handler prior to a listener that has been attached to the element itself.
window.addEventListener('click',function(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
console.log('window noticed you clicked something');
console.log(target);//<-- this is the element that was clicked
}, false);//<-- we'll get to the false in a minute
It's important to note we actually have access to the event object in the handlers. In this case, we left the event object untouched, so it'll just continue to propagate down to the target, on its way down, it might meet with something like this:
document.getElementById('container').addEventListener('click', function(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
if (target.tagName.toLowerCase() !== 'a' || target.className.match(/\bclickable\b/))
{
return e;//<return the event, unharmed
}
e.returnValue = false;
if (e.preventDefault)
{
e.preventDefault();
}
}, false);
Now, this handler will be called after the listener at the window level calls its helper. This time, the event is changed if the clicked element didn't have the clickable class, or the element is a link. The event is canceled, but it lives on, still. The event is still free to propagate further down the dom, so we might encounter something like:
document.getElmentById('form3').addEventListener('click',function(e)
{
e = e || window.event;
if (e.returnValue === false || e.isDefaultPrevented)
{//this event has been changed already
//do stuff, like validation or something, then you could:
e.cancelBubble = true;
if (e.stopPropagation)
{
e.stopPropagation();
}
}
}, false);
Here, by calling stopPropagation, the event is killed off. It can't propagate further down the dom to its target unless the event was already altered. If not, the event object travels further down the DOM, as if nothing happened.
Once it reaches its target node, the event enters its second phase: the bubble phase. Instead of propagating down into the deeps of the DOM, it climbs back up, to the top level (all the way to the global object, where it was dispatched... from whence it came and all that).
In the bubble phase, all the same rules apply as in the propagation phase, only the other way around. The event object will encounter the elements that are closest to the target element first, and the global object last.
There's a lot of handy, and clear diagrams for this here. I can't put it any better than good 'ol quirksmode, so I suggest you read what they have to say there.
Bottom line: when dealing with 2 event listeners, attach them both on a different level to sort-of queue them the way you like.
If you want to guarantee both are called, only stop the event from propagating in that handler that will be called last.
When you've got two listeners, attached to the same element/object for the same event, I've never come across a situation where the listener that was attached first, wasn't also called first.
That's it, I'm off to bed, hoping I made sense
jQuery makes this easy.
$(document).on('click', '.someclass', function() {
doStuff();
});
$(document).on('click', '.someclass', function() {
doMoreStuff();
});
Handlers then both will fire on click. jQuery keeps a queue of handers for you. And handles document clicks that match a selector of your choice so that they can be triggered no matter when your buttons are created.
I am/was having a similar issue as this. However I can not affect the order of/delegate the pre-existing 'click' events (added by Wicket framework).
But I still need to execute a new custom event before any of the 'click' or 'change' events handled by the framework.
Luckily there are several events that are actually executed in order. The 'mousedown' and the 'mouseup' happens to happen before the 'click'.
http://en.wikipedia.org/wiki/DOM_events
$(document).on('mousedown', function (event) {
event = event || window.event
var target = event.target || event.srcElement;
console.log(target + ' before default event'); // Hold mouse button down to see this message in console before any action is executed
});
OR
$(document).on('mouseup', function (event) {
event = event || window.event
var target = event.target || event.srcElement;
alert(target + ' before default event'); // You may not notice this event fires when the page changes unless this is an alert
});
This will allow the logging to be done (e.g. via ajax) before the actual event is executed e.g. a page change via (ajax) link.
Of course you may need to have more sophisticated means to detect for what the additional event handling should be done, but you can use for example the 'target' information for this. => This script monitors everything on the page, as this is how I need this to be done.

Categories