Vanilla JS Reusable Dynamic Function taken as a string - javascript

I'm trying to make a reusable function that can take in a dynamic "action" for reusability...
In this case, adding the background color is the action.
const button = document.querySelector("#button");
const items = document.querySelectorAll(`*[id^="eventHandlerCreatedItem"]`);
var eventHandler = (refEl, event, focusEls, action) => {
refEl.addEventListener(`${event}`, () => {
focusEls.forEach((focusEl) => {
// focusEl.style.backgroundColor = "orange"; // works
focusEl.`${action}`; // doesn't work
});
});
};
eventHandler(button, "click", items, 'style.backgroundColor = "orange"');
Thanks!

Don't use a string for this. An "action" semantically describes a "function", use a function:
var eventHandler = (refEl, event, focusEls, action) => {
refEl.addEventListener(`${event}`, () => {
focusEls.forEach((focusEl) => {
action(focusEl);
});
});
};
eventHandler(button, "click", items, (el) => el.style.backgroundColor = "orange");

Related

How to find all matching classes with Javascript and allow them all to run a function

The below will only find the first .available class and run my fuction. I need it to find all .available classes so that all of them can fun the function.
How would I do this?
this.DOM = {};
this.DOM.available = this.DOM.product.querySelector('.available');
this.DOM.available.addEventListener('click', () => this.open());
Full Code
class Item {
constructor(el) {
this.DOM = {};
this.DOM.el = el;
this.DOM.product = this.DOM.el.querySelector('.product');
this.DOM.productBg = this.DOM.product.querySelector('.product__bg');
this.DOM.productImg = this.DOM.product.querySelector('.product__img');
this.DOM.available = this.DOM.product.querySelectorAll('.available');
this.info = {
img: this.DOM.productImg.src,
title: this.DOM.product.querySelector('.product__title').innerHTML,
subtitle: this.DOM.product.querySelector('.product__subtitle').innerHTML,
// description: this.DOM.product.querySelector('.product__description').innerHTML,
price: this.DOM.product.querySelector('.product__price').innerHTML
};
this.initEvents();
}
open() {
DOM.details.fill(this.info);
DOM.details.open({
productBg: this.DOM.productBg,
productImg: this.DOM.productImg
});
}
initEvents() {
this.DOM.available.addEventListener('click', () => this.open());
}
};
Use .querySelectorAll() to get a collection of all matching elements and then use .forEach() to loop over the collection and assign the event handler:
this.DOM = {};
this.DOM.available = this.DOM.product.querySelectorAll('.available');
this.DOM.available.forEach(function(item){
item.addEventListener('click', () => item.open());
});

Removing event listener using loop removes only last element's listener

I have 3 elemenets in HTML collection and attach onclick listeners to them using forEach loop
[].forEach.call(signBoxes, (e, i) => {
e.addEventListener("click", callSetSign = () => setSign(signs[i]));
});
function setSign({ name, src }) {
const sign = name;
const Img = src;
player1Choice.src = Img;
[].forEach.call(signBoxes, (e, i) => {
e.removeEventListener("click", callSetSign);
});
socket.emit("self-choose-sign", sign);
}
Adding listeners works fine, however when I try to remove them in the same way, only the last element's listener is removed. If I alter the function like this, i get the same result.
function setSign({ name, src }) {
const sign = name;
const Img = src;
player1Choice.src = Img;
signBoxes[0].removeEventListener('click', callSetSign)
signBoxes[1].removeEventListener('click', callSetSign)
signBoxes[2].removeEventListener('click', callSetSign) // only this one works
socket.emit("self-choose-sign", sign);
}
Can someone explain this?
You can use an element's dataset object to pass parameters of type string to the event handler.
Obviously attempts to place parameters on the function object will simply over-write a property of the same name, and the .bind method does not return the function it was called on. Attempts to pass parameters in a closure or by calling bind will result in multiple handler function objects.
An untested example of the dataset approach:
[].forEach.call(signBoxes, (e, i) => {
e.dataset.index = i; // store index on element
e.addEventListener("click", setSign);
});
function setSign() {
const { name, src } = signs[this.dataset.index];; // lookup using index
const sign = name;
const Img = src;
player1Choice.src = Img;
[].forEach.call(signBoxes, (e, i) => {
e.removeEventListener("click", setSign);
});
socket.emit("self-choose-sign", sign);
}

React dynamically add html element with event handler

Is it possible to append buttons with event handler in react ?
// This is working fine but its pure javascript onclick , how can I make this element with react js onClick ?
This is reference code , my scenario is appending html code dynamically
const Button = () => {
const dynamicElement = () => {
let li = document.createElement('li');
li.className = 'dynamic-link'; // Class name
li.innerHTML = "I am New"; // Text inside
document.getElementById('links').appendChild(li);
li.onclick = function(){ alert(0) }
}
useEffect(() => {
dynamicElement();
}, [])
}
You can just convert your function to a functional component like this.
const DynamicElement = () => {
return (
<li onClick={()=>alert(0)} className="dynamic-link">I am New</li>
)
}
const Button = () => {
const [visible, setVisible] = useState(false);
useEffect(()=>setVisible(true), [])
// if visible is true return <DynamicElement/>, either way return null
return visible ? <DynamicElement/> : null
}
BTW useState is hooks implementation of a state

Override a function already attached to an event

I want to override a function already attached to an event. Like that :
let orFn = () => {
console.log('old');
};
buttonEl.addEventListener('click', orFn);
orFn = () => {
console.log('new');
};
However, when I click on the button, the old function is still called.
I have seen this stackoverflow. I can't use a function wrapper in my case. And I want to understand why this is not working.
My code is available for testing here: jsfiddle.
One way is to remove first listener, then add another one
let orFn = () => {
console.log('old');
};
let orFnNew = () => {
console.log('new');
};
var buttonEl = document.querySelector('button')
buttonEl.addEventListener('click', orFn);
// remove old listener
buttonEl.removeEventListener('click', orFn);
// add another one
buttonEl.addEventListener('click', orFnNew);
<button>button</button>
Another way is to have one listener, that may call different functions inside. Example:
// button
const buttonEl = document.querySelector('button')
const orFn = () => {
console.log('old');
};
const orFnNew = () => {
console.log('new');
};
// function that will be called
let functionToCall = orFn;
buttonEl.addEventListener('click', (event) => { // single listener
functionToCall.call(event); // call a function with event
functionToCall = functionToCall === orFn ? orFnNew : orFn; // change function to be called
});
<button>button</button>
One way of using bind().
const btn = document.getElementById('btn');
let orFn = () => {
alert('old');
};
orFn = () => {
alert('new');
};
orFn.bind(orFn)
btn.addEventListener('click', orFn);
This will bind with new function and show new in alert Popup.

How can I remove an event listener no matter how the callback is defined

For years I ran into problems trying to remove an event listener in JavaScript. Often I would have to create an independent function as the handler. But that is just sloppy and, especially with the addition of arrow functions, just a pain.
I am not after a ONCE solution. This needs to work in all situations no matter HOW the callback is defined. And this needs to be raw JS so anyone can use it.
The following code works fine since the function clickHandler is a unique function and can be used by both addEventListener and removeEventListener:
This example has been updated to show what I have run into in the past
const btnTest = document.getElementById('test');
let rel = null;
function clickHandler() {
console.info('Clicked on test');
}
function add() {
if (rel === null) {
rel = btnTest.addEventListener('click', clickHandler);
}
}
function remove() {
btnTest.removeEventListener('click', clickHandler);
}
[...document.querySelectorAll('[cmd]')].forEach(
el => {
const cmd = el.getAttribute('cmd');
if (typeof window[cmd] === 'function') {
el.addEventListener('click', window[cmd]);
}
}
);
<button cmd="add">Add</button>
<button cmd="remove">Remove</button>
<button id="test">Test</button>
You used to be able to do it with arguments.callee:
var el = document.querySelector('#myButton');
el.addEventListener('click', function () {
console.log('clicked');
el.removeEventListener('click', arguments.callee); //<-- will not work
});
<button id="myButton">Click</button>
But using an arrow function does not work:
var el = document.querySelector('#myButton');
el.addEventListener('click', () => {
console.log('clicked');
el.removeEventListener('click', arguments.callee); //<-- will not work
});
<button id="myButton">Click</button>
Is there a better way??
UPDATE
As stated by #Jonas Wilms this way will work:
var el = document.querySelector('#myButton');
el.addEventListener('click', function handler() {
console.log('clicked');
el.removeEventListener('click', handler); //<-- will work
});
<button id="myButton">Click</button>
Unless you need to using binding:
var obj = {
setup() {
var el = document.querySelector('#myButton');
el.addEventListener('click', (function handler() {
console.log('clicked', Object.keys(this));
el.removeEventListener('click', handler); //<-- will work
}).bind(this));
}
}
obj.setup();
<button id="myButton">Click</button>
The problem is that there are too many ways to provide an event handler to the addEventListener function and your code might break if the way you pass in the function changes in a refactor.
You can NOT use an arrow function or any anonymous function directly and expect to be able to remove the listener.
To remove a listener requires you pass the EXACT SAME ARGUMENTS to removeEventListener as you passed to addEventListener but when you use an anonymous function or an arrow function you do not have access to that function so it's impossible for you to pass it into removeEventListener
works
const anonFunc = () => { console.log("hello"); }
someElem.addEventListener('click', anonFunc);
someElem.removeEventListener('click', anonFunc); // same arguments
does not work
someElem.addEventListener('click', () => { console.log("hello"); });
someElem.removeEventListener('click', ???) // you don't have a reference
// to the anon function so you
// can't pass the correct arguments
// to remove the listener
your choices are
don't use anonymous or arrow functions
use a wrappers that will track the arguments for you
One example is #Intervalia closure. He tracks the function and other arguments you passed in and returns a function you can use the remove the listener.
One solution I often use which often fits my needs is a class that tracks all the listeners and remove them all. Instead of a closure it returns an id but it also allows just removing all listeners which I find useful when I build up something now and want to tear it down something later
function ListenerManager() {
let listeners = {};
let nextId = 1;
// Returns an id for the listener. This is easier IMO than
// the normal remove listener which requires the same arguments as addListener
this.on = (elem, ...args) => {
(elem.addEventListener || elem.on || elem.addListener).call(elem, ...args);
const id = nextId++;
listeners[id] = {
elem: elem,
args: args,
};
if (args.length < 2) {
throw new Error('too few args');
}
return id;
};
this.remove = (id) => {
const listener = listeners[id];
if (listener) {
delete listener[id];
const elem = listener.elem;
(elem.removeEventListener || elem.removeListener).call(elem, ...listener.args);
}
};
this.removeAll = () => {
const old = listeners;
listeners = {};
Object.keys(old).forEach((id) => {
const listener = old[id];
if (listener.args < 2) {
throw new Error('too few args');
}
const elem = listener.elem;
(elem.removeEventListener || elem.removeListener).call(elem, ...listener.args);
});
};
}
Usage would be something like
const lm = new ListenerManager();
lm.on(saveElem, 'click', handleSave);
lm.on(newElem, 'click', handleNew);
lm.on(plusElem, 'ciick', handlePlusOne);
const id = lm.on(rangeElem, 'input', handleRangeChange);
lm.remove(id); // remove the input event on rangeElem
lm.removeAll(); // remove events on all elements managed by this ListenerManager
note the code above is ES6 and would have to be changed to support really old browsers but the ideas are the same.
Just use a named function expression:
var el = document.querySelector('#myButton');
el.addEventListener('click', function handler() {
console.log('clicked');
el.removeEventListener('click', handler); //<-- will work
});
For sure that can be wrapped in a function:
function once(selector, evt, callback) {
var el = document.querySelector(selector);
el.addEventListener(evt, function handler() {
callback();
el.removeEventListener(evt, handler); //<-- will work
});
}
once("#myButton", "clicl", () => {
// do stuff
});
There is an easy solution using closures.
By moving the code to both addEventListener and removeEventListener into a single function you can accomplish the task easily:
function ael(el, evt, cb, options) {
console.log('Adding', evt, 'event listener for', el.outerHTML);
el.addEventListener(evt, cb, options);
return function() {
console.log('Removing', evt, 'event listener for', el.outerHTML);
el.removeEventListener(evt, cb, options);
}
}
const btnTest = document.getElementById('test');
let rel = null;
function add() {
if (rel === null) {
rel = ael(btnTest, 'click', () => {
console.info('Clicked on test');
});
}
}
function remove() {
if (typeof rel === 'function') {
rel();
rel = null;
}
}
function removeAll() {
rels.forEach(rel => rel());
}
const rels = [...document.querySelectorAll('[cmd]')].reduce(
(rels, el) => {
const cmd = el.getAttribute('cmd');
if (typeof window[cmd] === 'function') {
rels.push(ael(el, 'click', window[cmd]));
}
return rels;
}, []
);
<button cmd="add">Add</button>
<button cmd="remove">Remove</button>
<button id="test">Test</button>
<hr/>
<button cmd="removeAll">Remove All</button>
The function ael above allows the element, the event type and the callback to all be saved in the closure scope of the function. When you call ael it calls addEventListener and then returns a function that will call removeEventListener. Later in your code you call that returned function and it will successfully remove the event listener without worrying about how the callback function was created.
Here is an es6 version:
const ael6 = (el, evt, cb, options) => (el.addEventListener(evt, cb, options), () => el.removeEventListener(evt, cb, options));
You can use the once option of EventTarget.addEventListener():
Note: supported by all browsers but IE.
var el = document.querySelector('#myButton');
el.addEventListener('click', () => {
console.log('clicked');
}, { once: true });
<button id="myButton">Click</button>

Categories

Resources