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.
Related
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);
Got some simple functionality set up on a page. Initially I want to replace default action of a hyperlink click with some functionality which will display an overlay.
After the overlay is displayed I want to remove the event listener I have placed on the hyperlink so it reverts to what it was previously (i believe there is another event listener on here, I dont want to remove this one while removing mine). Within the overlay is another button which when clicked, should trigger the initial functionality of the button.
Ive tried the .off() jquery method, however this seems to prevent the ".mmclose" button from working.
Not quite sure where i am going wrong with this..
// placing event listener on initial link
$("#utility_0_HyperLinkLogout").click(function() {
// removing event listener(?)
$("#utility_0_HyperLinkLogout").off("click");
// preventing default button behavior
event.preventDefault();
//overlay replacing original content
I62originalContent.hide();
$("#mmi62wrapper").fadeIn("slow", function() {
// new event listener placed on button within overlay (as callback)
$(".mmclose").click(function() {
//new button should now trigger original buttons original functionality?
$("#utility_0_HyperLinkLogout").trigger("click");
})
})
});
You can use jQuery .one method to attach a handler which will be executed only once. You don't need to worry about removing this handler anymore.
Check this example:
$(".myClass").click(function() {
this.innerText += "!";
});
$("#myId").one('click', function() {
this.innerText += "?";
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button class="myClass" id="myId">Click me twice</button>
In this example, clicking the button keeps adding "!", while "?" is only added once. Other handlers are not affected.
Is there a way to be notified or perform some callback function once an event has finished propagating in JavaScript?
Equivalently, and more specifically: is there a way to 'prioritize' an event and make sure that it is called after every other event listener has been fired (similarly almost to the !important value in CSS).
For instance, if I have 3 event listeners - 2 attached to the window and 1 to some button element. Can I force a certain one of those events to be called LAST, regardless of where it lies in the DOM? I understand that there are event phases and the ability to attach a listener to the capture or bubbling phase but this still means there's a preset order.
edit: the specific problem:
I'm attempting to build components (in React JS) which are aware of a click being registered outside of themselves (i.e. anywhere on the window/document except themselves) - often as a way of closing/hiding the component. Each of these components will register a listener on the window object which fires a function belonging to that component.
The trouble is, when another component [B] (inherently lower down in the DOM than the window) is clicked to let's say toggle the display of [A], [B]'s event fires first and toggles the state 'showA', the event bubbles up and [A]'s window event listener kicks in and re-toggles the state 'showA' - so, [A] remains hidden after changing state twice. I can't use stopPropagation as other window events need to fire. I've tried to unbind listeners but this doesn't happen in time.
An example of what currently happens all in one go is:
'show thing' button clicked
add listener to window for closing 'thing'
'window but not thing' was clicked
remove listener to close 'thing'
If only I could wait until the click event had finished bubbling before adding the new listener, I'd have no issue
I did leave an answer to your original question but I see you've updated it. I wouldn't say this is React specific but a common implementation for components that need to close/de-activate when the document is clicked.
For instance, the following snippet is an implementation for a speed dial spin out button;
(function () {
var VISIBLE_CLASS = 'is-showing-options',
btn = document.getElementById('.btn'),
ctn = document.getElementById('.ctn'),
showOpts = function(e) {
var processClick = function (evt) {
if (e !== evt) {
ctn.classList.remove(VISIBLE_CLASS);
ctn.IS_SHOWING = false;
document.removeEventListener('click', processClick);
}
};
if (!ctn.IS_SHOWING) {
ctn.IS_SHOWING = true;
ctn.classList.add(VISIBLE_CLASS);
document.addEventListener('click', processClick);
}
};
btn.addEventListener('click', showOpts);
}.call(this));
When the user clicks the button, the container is shown for the speed dial options and an event listener is bound to the document. However, you need to make sure that the initial event that is fired is not the one that triggers the takedown straight away (this is sometime a gotcha). This check is made with if (e !== evt) .... For further clicks the event check is made and the relevant action taken ending in removal of the event listener from the document.
Of course in your particular case if you want to only close when the element isn't clicked then you could make relevant checks on the evt.target and evt.currentTarget in the callback (in the snippet case, processClick).
Hopefully, this can help you out with registering close down callbacks for your individual components.
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);
}
I have a table of cells, when a cell is clicked an event is triggered. I want to add cells dynamically so I will call OnClick again on all rows. When I call OnClick for the second time, any cells that have OnClick called twice stop firing any events.
The odd thing is at the event of my OnClick function, if I can "Return;" it works, however it throws an error saying "Return" isn't defined
function initBox() {
$(".selectable").on('click', function (event) {
//if its selected already, unselect it
if ($(this).hasClass('rowHighlightColor')) {
$(this).removeClass("rowHighlightColor");
selectedCellList = null;
}
else {
//unselect previous cell
if (selectedCellList != null) {
selectedCellList.removeClass("highlighted");
}
selectedCellList = $(this);
$(this).addClass("rowHighlightColor");
}
Return;
});
}
it needs to be return instead of Return (capital R)
however, writing return; returns undefined so you can just omit it.
edit:
you attach the event twice, make sure only to attach it once, else it causes (as you noticed) undesired behaviour like attaching class and immediately removing it again.
The reason why it works with "Return" is that the function is only run once because it throws an error when reaching it.
Use jQuerys .live() (or .on() for newer versions, as live is deprecated there) to automatically attach the click event to every new row you add. jQuery live Docs
You are adding multiple event handlers to existing cells. This is one reason why I prefer to use just the plain old .onclick property.
Anyway, to solve this issue you can either only apply the event to the new cells, or add an attribute to them when you do add an event, then check that attribute before adding the event again.