Adding and Removing Event Listeners Dynamically - javascript

I am having issues dynamically adding and removing eventListeners. I want to be able to add an event listener to all child nodes of an element. Then at a later time remove them, and add them back.
Note: I am aware of EventListener options.once however this doesn't exactly solve my case.
Here is some sample code of what I am trying to do:
var cont;
window.onload = function() {
cont = document.querySelector(".container");
[...cont.children].forEach(c => {
c.addEventListener("click", clicked.bind(c))
});
}
function clicked() {
console.log(`removing event listener from ${this}`);
this.removeEventListener("click", clicked); //Not actually removing - why?
}
Thanks everyone!

The problem is that .bind creates a new function. The function passed to addEventListener can only be removed if that exact same function is passed to removeEventListener. A bound function is not the same as the original unbound function, so removeEventListener won't work if you pass it the unbound function.
In your situation, one possibility would be to use a Map indexed by the HTML elements, whose value is the bound listener for that element, so that they can then be removed later:
const listenerMap = new Map();
const cont = document.querySelector(".container");
[...cont.children].forEach(c => {
const listener = clicked.bind(c);
listenerMap.set(c, listener);
c.addEventListener("click", listener);
});
function clicked() {
console.log(`removing event listener from ${this}`);
this.removeEventListener("click", listenerMap.get(this));
}
<div class="container">
<div>one</div>
<div>two</div>
<div>three</div>
</div>

Related

Lit element, close drop down when click outside of component

I'm working with Lit Element and I'm trying add an event listener on 'Click' that will a variable state that will set the dropdown to be expand or not. But once the drop down is 'closed' I want to remove that event to avoid unnecessary event calls on 'Click.
Adding the event works great but I cannot remove it.
Here is the idea:
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("_tenantsExpanded")) {
document.removeEventListener("click", (ev) => this._eventLogic(ev, this));
if (this._tenantsExpanded)
document.addEventListener("click", (ev) => this._eventLogic(ev, this));
}
}
The fct logic:
private _eventLogic(e: MouseEvent, component: this) {
const targets = e.composedPath() as Element[];
if (!targets.some((target) => target.className?.includes("tenant"))) {
component._tenantsExpanded = false;
}
}
Code in my render's function:
${this._tenantsExpanded
? html` <div class="tenants-content">${this._tenantsContent()}</div> `
: html``}
Important note: I want the click event to be listened on all the window, not just the component itself. The same for removing the event.
PS: I don't know why e.currentTaget.className doesn't give me the actual className, but results to an undefined.
When you use removeEventListener you have to pass a reference to the same function you used when adding the listener.
In this example the function is stored in fn.
(You might have to change the this reference here, it depends a bit on your whole component).
const fn = (ev) => this._eventLogic(ev, this);
document.addEventListener("click", fn);
document.removeEventListener("click", fn);

Remove event listener in ES6

I have following code:
document.getElementsByClassName('drag')[i].addEventListener('mousedown', (e) => {
console.log('mousedown' + i);
});
It would be easy, if I would be able to name the function inside listener. But it's not possible in my case.
It would look like:
e.currentTarget.removeEventListener(e.type, nameFunction);
Is there any way to make it work?
Thank you.
Yes you can write like this.
document.getElementsByClassName('drag')[i].addEventListener('mousedown', mouseDownFun);
function mouseDownFun(e){
console.log('mousedown' + i);
}
e.currentTarget.removeEventListener(e.type, mouseDownFun);
So whenever mouse down event will be triggered it will listen in mouseDownFun.
It would be easy, if I would be able to name the function inside listener. But it's not possible in my case.
Don't use an arrow function if it doesn't allow you to do what you want.
document.getElementsByClassName('drag')[i].addEventListener('mousedown', function handler(e) {
console.log('mousedown' + i);
e.currentTarget.removeEventListener(e.type, handler);
});
There are two ways
When you define a function, then add and remove the listener when you want:
// Define a function
const handler = () => {
console.log('Click event...');
}
// Select the element and add event listener
const img = document.querySelector('img');
img.addEventListener('click', handler, false);
// ...
// Remove the event listener
img.removeEventListener('click', handler, false);
When you add an event listener and remove it on event:
// Select the element and add event listener
const img = document.querySelector('img');
img.addEventListener('click', function handler(event) {
console.log('Click event...');
// On event, remove the event listener
event.currentTarget.removeEventListener(event.type, handler);
});

How do I add event listeners to HTML elements if I do not know when they will be created?

I would like to separate my code from my UI in my web page and I would like to know if there is an event I can use that will tell me if an element is created so I can assign an event handler to it at the time it's created.
For example, if I had two elements, and they each have event handlers, how can I tell when those elements have been created and then add them?
What I have so far is to add a listener to the document to a addElement event, if one exists. I don't know if one exists. If it does than, check if the element matches any existing id's in a dictionary of functions. So something like this:
Code from separate script file:
<script>
// event handler for BorderContainer1282
function doSomething1() {
}
// event handler for Button925
function doSomething2() {
}
var clickHandlersDictionary = {};
clickHandlersDictionary["BorderContainer1282"] = doSomething1;
clickHandlersDictionary["Button925"] = doSomething2;
function createComplete() {
document.addEventListener("added", addElementEventHandlers);
}
function addElementEventHandlers(event) {
var element = event.currentTarget;
if (clickHandlersDictionary[element.id]!=null) {
element.addEventListener("click", clickHandlersDictionary[element.id]);
}
}
</script>
And the HTML:
<body onReadyState="creationComplete()">
<div id="BorderContainer1282">
<input id="Button925" type="button" value="Button">
</div >
</body>
I think what I might be trying to do is dependency injection but I'm not sure. I may eventually try and support states and that case event listeners would need to be added and also removed when an HTML element is created.
You can use a MutationObserver. MutationObservers allow you to detect changes to DOM and react accordingly.
Take the following for example:
/*****
Setting up some of your code
*****/
function doSomething1() {
console.log('Do something 1');
}
function doSomething2() {
console.log('Do something 2');
}
var dictionary = {
BorderContainer1282: doSomething1,
Button925: doSomething2
};
/*****
Setting up the observer
*****/
// select the target node
var target = document.getElementById('target');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
Object.keys(dictionary).forEach(function(key) {
var query = target.querySelector('#' + key);
if (query) {
query.addEventListener('click', dictionary[key]);
}
});
});
});
// pass in the target node, as well as the observer options
observer.observe(target, {
childList: true
});
/*****
Just adds to the DOM when you click on the button
*****/
function addToDom(button) {
button.parentNode.removeChild(button);
var parentDiv = document.createElement('div');
parentDiv.textContent = 'Parent';
var childDiv1 = document.createElement('div');
childDiv1.textContent = 'BorderContainer1282 (Click Me)';
childDiv1.id = 'BorderContainer1282';
var childDiv2 = document.createElement('div');
childDiv2.textContent = 'Button925 (Click Me)';
childDiv2.id = 'Button925';
parentDiv.appendChild(childDiv1);
parentDiv.appendChild(childDiv2);
target.appendChild(parentDiv);
}
<button onclick="addToDom(this)">Add to DOM</button>
<div id="target"></div>
The MutationObserver picks up changes to the target element (which could be any element, including just the body). In this particular case, it searches the added nodes for any matches to elements in the dictionary. If if finds them, it adds the corresponding function to the click listener.
You need to use event delegation.
If your target div, where you expect new elements to be created, has an id of tgt, you could code an event listener like so:
document.getElementById("tgt").addEventListener("click",function(event){
alert(event.target.id);
});

Unexpected scoping issue when assigning click listener

When my loadNav() function loops through the nav array and assigns a click listener to a dynamically-created element, all seems well. If I break on the first loop through the navItem.id is 'newGL' and the second time through it is 'litGL', as it should be.
But when I check the listeners after the loop is done, they both have an id of 'litGL' (and also a type of 'b'). But the elements themselves have the correct navItem.text, so I guess I’m not understanding how variables are assigned within the passed function to the .click method.
Any hints why my first click listener is getting overwritten with the values meant for the second one?
var nav = [
{
id:'newGL',
text:'new',
type:'a'
},
{
id:'litGL',
text:'lit',
type:'b'
}
]
function loadNav(nav){
for(item in nav){
var navItem = nav[item];
var element = $("<div>" + navItem.text + "</div>");
element.click(function(){
getContent(navItem.id,navItem.type);
//in practice I'm getting two click listeners with a navItem.id of 'litGL' and a navItem.type of 'b'
});
$('#horzNav').append(element);
}
}
The immediate problem is that the navItem is not the same value when the event is triggered much later. To fix this common issue use a scoping IIFE (immediately invoked function expression):
function loadNav(nav){
for(item in nav){
var navItem = nav[item];
var element = $("<div>" + navItem.text + "</div>");
(function(navItem){
element.click(function(){
getContent(navItem.id,navItem.type);
//in practice I'm getting two click listeners with a navItem.id of 'litGL' and a navItem.type of 'b'
});
})(navItem);
$('#horzNav').append(element);
}
}
Better solutions involve putting the require attributes into the injected elements, then extracting them at event time. This simplifies the event handler and removes the dependency on the original navItem variable.
e.g. something like:
function loadNav(nav){
for(var i = 0; i < nav.length; i++){
var navItem = nav[i];
var element = $("div", {id: navItem.id}).html(navItem.text).data('type', navitem.type ).appendTo('#horzNav');
}
}
and use a delegated event handler with a selector:
$(function () {
$(document).on("click", ".navItem", function () {
getContent($(this).attr("id"), $(this).data("type"));
});
});
This works by listening for the event (e.g. click) to bubble up to a non-changing ancestor element (e.g. document), then applying the selector to the items in the bubble-chain, then applying the function only to those matching element that caused the event.
The upshot of this is the the items only need to match at event time and not when the event was registered. Great for dynamically added items.
document is the best default if no other ancestor else is closer/convenient. Do not use body for delegated events as it has a bug (styling can cause it to not get mouse events).
Your issue is scoping, but you really need to delegate that event (otherwise if you had 100 elements, that'd be 100 events :/), then you can specify the properties you need within the markup using data attributes, something like:
$(function () {
$(document).on("click", ".navItem", function () {
getContent($(this).attr("id"), $(this).data("type"));
});
});
Then your loadNav would be:
function loadNav(nav){
for(item in nav){
var navItem = nav[item];
var element = $(document.createElement("div"));
element.html(navItem.text);
element.prop("id", navItem.id);
element.data("type", navItem.type);
$('#horzNav').append(element);
}
}

dojo how to overwrite event handler

Currently dojo uses on method to connect event to handler.
btn = new Button();
btn.on('click', function () {console.log('do something');});
this will call the attached function when the button gets clicked.
however, according to the documents, removing existing handlers should be done in the following way
handler = btn.on('click', function () {console.log('do something');});
handler.remove();
this is not the way I want to remove event handler.
I do not store the handler reference anywhere. But I want to add a new 'click' event by doing
btn.on('click', function () {console.log('do something different');});
so that it replaces the existing 'click' event handler and add a new one.
Is there any way to achieve what I want?
Thanks!
That's not possible, the framework tells you to do it in the way by creating a reference to the event handler. This is similar to how other frameworks like jQuery work.
jQuery has of course a mechanism to remove all event handlers by using the off() function, but that's not available in Dojo either. Like Chris Hayes suggested in the comments, you can implement such a feature by yourself, either by wrapping it inside another module, or by using aspects on the dojo/on module.
For example, you can wrap it inside a new module:
// Saving the event handlers
var on2 = function(dom, event, callback) {
on2.handlers = [];
if (on2.handlers[event] === undefined) {
on2.handlers[event] = [];
}
var handler = on(dom, event, callback);
on2.handlers[event].push({
node: dom,
handler: handler
});
return handler;
};
// Off functionality
lang.mixin(on2, on, {
off: function(dom, event) {
if (this.handlers[event] !== undefined) {
array.forEach(this.handlers[event], function(handler) {
if (handler.node === dom) {
handler.handler.remove();
}
});
}
}
});
And then you can use it:
on2(dom.byId("test"), "click", function() {
console.log("test 1 2 3"); // Old event handler
});
on2.off(dom.byId("test"), "click"); // Remove old event handlers
on2(dom.byId("test"), "click", function() {
console.log("test 4 5 6"); // New event handler
});
This should work fine, as you can see in this fiddle: http://jsfiddle.net/X7H3F/
btn = new Button();
btn.attr('id','myButton');
query("#myButton").on('click', function () {console.log('do something');});
Do the same thing when you want to replace your handler. Like,
query("#myButton").on('click', function () {console.log('do something different');});
Hope that helps :)

Categories