addEventListener on NodeList [duplicate] - javascript

This question already has answers here:
Adding click event listener to elements with the same class
(5 answers)
Closed 8 months ago.
Does NodeList support addEventListener. If not what is the best way to add EventListener to all the nodes of the NodeList. Currently I am using the code snippet as show below, is there a better way to do this.
var ar_coins = document.getElementsByClassName('coins');
for(var xx=0;xx < ar_coins.length;xx++)
{
ar_coins.item(xx).addEventListener('dragstart',handleDragStart,false);
}

There is no way to do it without looping through every element. You could, of course, write a function to do it for you.
function addEventListenerList(list, event, fn) {
for (var i = 0, len = list.length; i < len; i++) {
list[i].addEventListener(event, fn, false);
}
}
var ar_coins = document.getElementsByClassName('coins');
addEventListenerList(ar_coins, 'dragstart', handleDragStart);
or a more specialized version:
function addEventListenerByClass(className, event, fn) {
var list = document.getElementsByClassName(className);
for (var i = 0, len = list.length; i < len; i++) {
list[i].addEventListener(event, fn, false);
}
}
addEventListenerByClass('coins', 'dragstart', handleDragStart);
And, though you didn't ask about jQuery, this is the kind of stuff that jQuery is particularly good at:
$('.coins').on('dragstart', handleDragStart);

The best I could come up with was this:
const $coins = document.querySelectorAll('.coins')
$coins.forEach($coin => $coin.addEventListener('dragstart', handleDragStart));
Note that this uses ES6 features, so please make sure to transpile it first!

There actually is a way to do this without a loop:
[].forEach.call(nodeList,function(e){e.addEventListener('click',callback,false)})
And this way is used in one of my one-liner helper libraries - nanoQuery.

The simplest example is to add this functionality to NodeList
NodeList.prototype.addEventListener = function (event_name, callback, useCapture)
{
for (var i = 0; i < this.length; i++)
{
this[i].addEventListener(event_name, callback, useCapture);
}
};
Now you can do:
document.querySelectorAll(".my-button").addEventListener("click", function ()
{
alert("Hi");
});
In the same way, you can do a forEach loop
NodeList.prototype.forEach = function (callback)
{
for (var i = 0; i < this.length; i++)
{
callback(this[i], i);
}
};
Using:
document.querySelectorAll(".buttons").forEach(function (element, id)
{
input.addEventListener("change", function ()
{
alert("button: " + id);
});
});
EDIT : note that NodeList.prototype.forEach has existed ever since november 2016 in FF. No IE support though

in es6, you can do a array from nodelist, using Array.from, e.g.
ar_coins = document.getElementsByClassName('coins');
Array
.from(ar_coins)
.forEach(addEvent)
function addEvent(element) {
element.addEventListener('click', callback)
}
or just use arrow functions
Array
.from(ar_coins)
.forEach(element => element.addEventListener('click', callback))

Another solution is to use event delegation. You just use addEventListener to the closest parent of the .coins elements and use event.target in the callback to check if the click was really on an element with the class "coins".

I suppose another option would be to define addEventListener on NodeList using Object.defineProperty. That way you can treat the NodeList as you would a single Node.
As an example, I created a jsfiddle here: http://jsfiddle.net/2LQbe/
The key point is this:
Object.defineProperty(NodeList.prototype, "addEventListener", {
value: function (event, callback, useCapture) {
useCapture = ( !! useCapture) | false;
for (var i = 0; i < this.length; ++i) {
if (this[i] instanceof Node) {
this[i].addEventListener(event, callback, useCapture);
}
}
return this;
}
});

You could also use prototyping
NodeList.prototype.addEventListener = function (type, callback) {
this.forEach(function (node) {
node.addEventListener(type, callback);
});
};

Related

Selecting and modifying DOM elements

How do I select DOM elements in JS? What is the equivalent for jQuery's $ selection syntax?
For example I have a <div> element:
<div id="idDiv">Div Element</div>
Now I want to apply addClass("ClassName") jQuery function on div.
I can do it with jQuery with the following way:
$("#idDiv").addClass("ClassName") or jQuery("#idDiv").addClass("ClassName")
How can I do this with vanilla JS?
You can use the classList API:
// Adding classes
document.getElementById('idDiv').classList.add('foo');
// Toggling classes
document.getElementById('idDiv').classList.toggle('foo');
// Removing classes
document.getElementById('idDiv').classList.remove('bar');
Please note that IE9 and below do not support the API, for supporting those browsers you can use a shim, MDN has one.
An experimental solution:
function jFoo(selector) {
return {
elems: [].slice.call(document.querySelectorAll(selector)),
_handleClass: function (cls, m) {
var len = this.elems.length,
cls = cls.trim().split(/\s/),
clen = cls.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < clen; j++)
this.elems[i].classList[m](cls[j]);
}
return this;
},
addClass: function (cls) {
return this._handleClass(cls, 'add');
},
toggleClass: function (cls) {
return this._handleClass(cls, 'toggle');
},
removeClass: function (cls) {
return this._handleClass(cls, 'remove');
},
}
}
jFoo('selector').toggleClass('foo bar')
.addClass('barz fool')
.removeClass('foo');
You can get element by javascript in following way:
var getelem = document.getElementById("idDiv");
getelem.setAttribute("class", "active");
Another way of adding class using javascript:
document.getElementById("idDiv").className += " ClassName";

How to properly pass argument in loop to multiple event handlers? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Javascript closure inside loops - simple practical example
I add event handlers to multiple hrefs on my website with JS like this:
function addButtonListener(){
var buttons = document.getElementsByClassName("selLink");
for (var i = 0; i < buttons.length; i++)
{
button.addEventListener('click',function() { addTosel(i); },true);
}
}
}
But unfortunately to addTosel is passed the last i not the i from the loop. How to pass i accordingly to the object being processed in this moment?
You need to create a closure:
function addButtonListener(){
var buttons = document.getElementsByClassName("selLink");
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click', function(index) {
return function () {
addTosel(index);
};
}(i), true);
}
}
This way the scope of the handler is bound to the proper context of i.
See this article for more information on this subject.
You need to bind the i variable to the function when its declared. like so
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click',(function() { addTosel(this); }).bind(i) ,true);
}
Note: I just wrote the code from memory so it may not be perfect, but it is the sulution you're needing, for reference as to the proper way, ie with cross browser shims etc look at:
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
If you're going to take the .bind approach, do it like this.
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click', addTosel.bind(null, i), true);
}
This makes a new function with null bound as the this value since your function doesn't seem to need it, and the current i bound as the first argument.
Or make your own binder function
var _slice = Array.prototype.slice;
function _binder(func, ctx /*, arg1, argn */) {
var bound_args = _slice.call(arguments, 2);
return function() {
return func.apply(ctx, bound_args.concat(_slice.call(arguments)));
}
}
And then do this.
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click', _binder(addTosel, null, i), true);
}

Can I shrink this code with an array or loop?

Forgive me, I hope this question isn't too obvious, I'm a javascript noob.
I have javascript code that takes numbers from an xml sheet and displays them in td elements on an html page. It works but I think it could be condensed into an array or a loop to be more efficient.
Is there a better way to write this code?
window.onload=function displayPrices()
{
twentyFourK=(x[i].getElementsByTagName("twentyFourK")[0].childNodes[0].nodeValue);
document.getElementById("twentyFourK").innerHTML=toCurrency(twentyFourK);
oneOzGold=(x[i].getElementsByTagName("oneOzGold")[0].childNodes[0].nodeValue);
document.getElementById("oneOzGold").innerHTML=toCurrency(oneOzGold);
fiveOzGold=(x[i].getElementsByTagName("fiveOzGold")[0].childNodes[0].nodeValue);
document.getElementById("fiveOzGold").innerHTML=toCurrency(fiveOzGold);
tenOzGold=(x[i].getElementsByTagName("tenOzGold")[0].childNodes[0].nodeValue);
document.getElementById("tenOzGold").innerHTML=toCurrency(tenOzGold);
oneKiloGold=(x[i].getElementsByTagName("oneKiloGold")[0].childNodes[0].nodeValue);
document.getElementById("oneKiloGold").innerHTML=toCurrency(oneKiloGold);
//etc.
}
Yes, a function could make things much easier for you:
window.onload = function() {
function loadCurrency(name) {
document.getElementById(name).innerHTML = toCurrency(x[i].getElementsByTagName(name)[0].firstChild.nodeValue);
}
loadCurrency('twentyFourK');
loadCurrency('oneOzGold');
loadCurrency('fiveOzGold');
loadCurrency('tenOzGold');
loadCurrency('oneKiloGold');
};
Also, if you have many items to load:
window.onload = function() {
function loadCurrency(name) {
document.getElementById(name).innerHTML = toCurrency(x[i].getElementsByTagName(name)[0].firstChild.nodeValue);
}
var items = ['twentyFourK', 'oneOzGold', 'fiveOzGold', 'tenOzGold', 'oneKiloGold'];
items.forEach(loadCurrency);
};
That requires Array.forEach, which is only available in ECMAScript 5, so here's a fallback:
Array.prototype.forEach = function(action, thisArg) {
for(var i = 0, l = this.length; i < l; i++) {
if(i in this) {
action.call(thisArg, this[i], i, this);
}
}
};
I would place the currency setting into its own method. This will be cleaner visually and will also allow for implementation changes in the future:
window.onload = function displayPrices() {
SetCurrency("twentyFourK");
SetCurrency("oneOzGold");
//etc.
}
function SetCurrency(name) {
var elements = x[i].getElementsByTagName(name);
if ((elements != null) && (elements.length != 0)) {
elements[0].innerHTML = toCurrency(elements[0].childNodes[0].nodeValue);
}
}
You could create a function with a list of element in parameter, and you just have to create a loop going through your list of elements (twentyFourK, oneKiloGold and so on)

Pure JavaScript equivalent of jQuery click()?

I am building a small app which captures mouse clicks. I wrote the prototype in jQuery but, since it is a small app focusing on speed, embedding jQuery to use just one function would be an overkill.
I tried to adapt this example from JavaScriptKit:
document.getElementById("alphanumeric").onkeypress=function(e){
//blah..blah..blah..
}
but it didn't work when I tried this:
document.getElementsByTagName("x").onclick
What am I doing wrong?
Say you have a list of p tags you would like to capture the click for the <p> tag:
var p = document.getElementsByTagName("p");
for (var i = 0; i < p.length; i++) {
p[i].onclick = function() {
alert("p is clicked and the id is " + this.id);
}
}
Check out an example here for more clarity:
http://jsbin.com/onaci/
In your example you are using getElementsByTagName() method, which returns you an array of DOM elements. You could iterate that array and assign the onclick handler to each element, for example:
var clickHandler = function() {
alert('clicked!');
}
var elements = document.getElementsByTagName('div'); // All divs
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = clickHandler;
}
it looks a little bit like you miss more than just the click function of jQuery. You also miss jquery's selector engine, chaining, and automatic iteration across collections of objects. With a bit more effort you can minimally reproduce some of those things as well.
var myClickCapture = function (selector) {
var method, name,iterator;
if(selector.substr(0,1) === "#") {
method = "getElementById";
name = selector.substr(1);
iterator = function(fn) { fn(document[method](name)); };
} else {
method = "getElementsByTagName";
name = selector;
iterator = function(fn) {
var i,c = document[method](name);
for(i=0;i<c.length;i++){
fn(c[i]);
};
};
myClickCapture.click = function (fn){
iterator(function(e){
e.onclick=fn;
})
}
return myClickCapture;
}
I haven't tested the code, but in theory, it gets you something like this:
myClickCapture("x").click(function(e){ alert("element clicked") });
Hopefully this gives you a sense of the sorts of things jquery is doing under the covers.
document.getElementsByTagName("x")
returns an array of elements having the tagname 'x'.
You have to right event for each element in the returned array.

JavaScript: Adding an onClick handler without overwriting the existing one

I'm trying to modify all links on a page so they perform some additional work when they are clicked.
A trivial approach might be something like this:
function adaptLinks()
{
var links = document.getElementsByTagName('a');
for(i = 0; i != links.length; i++)
{
links[i].onclick = function (e)
{
<do some work>
return true;
}
}
}
But some of the links already have an onClick handler that should be preserved. I tried the following:
function adaptLinks()
{
var links = document.getElementsByTagName('a');
for(i = 0; i != links.length; i++)
{
var oldOnClick = links[i].onclick;
links[i].onclick = function (e)
{
if(oldOnClick != null && !oldOnClick())
{
return false;
}
<do some work>
return true;
}
}
}
But this doesn't work because oldOnClick is only evaluated when the handler is called (it contains the value of the last link as this point).
Don't assign to an event handler directly: use the subscribe model addEventListener / attachEvent instead (which also have remove pairs!).
Good introduction here.
You need to create a closure to preserve the original onclick value of each link:
Hi
There
<script type="text/javascript">
function adaptLinks() {
var links = document.getElementsByTagName('a');
for (i = 0; i != links.length; i++) {
links[i].onclick = (function () {
var origOnClick = links[i].onclick;
return function (e) {
if (origOnClick != null && !origOnClick()) {
return false;
}
// do new onclick handling only if
// original onclick returns true
alert('some work');
return true;
}
})();
}
}
adaptLinks();
</script>
Note that this implementation only performs the new onclick handling if the original onclick handler returns true. That's fine if that's what you want, but keep in mind you'll have to modify the code slightly if you want to perform the new onclick handling even if the original handler returns false.
More on closures at the comp.lang.javascript FAQ and from Douglas Crockford.
Use a wrapper around addEventListener (DOM supporting browsers) or attachEvent (IE).
Note that if you ever want to store a value in a variable without overwriting the old value, you can use closures.
function chain(oldFunc, newFunc) {
if (oldFunc) {
return function() {
oldFunc.call(this, arguments);
newFunc.call(this, arguments);
}
} else {
return newFunc;
}
}
obj.method = chain(obj.method, newMethod);
In Aspect Oriented Programming, this is known as "advice".
how about setting oldClick = links[i].onclick or an empty function. Like so
var oldOnClick = links[i].onclick || function() { return true; };
links[i].onclick = function (e)
{
if (!oldOnClick())
return false;
//<do some work>
return true;
}
Or you could use attachEvent and addEventListener as others have recommended
function addEvent(obj, type, fn) {
if (obj.addEventListener)
obj.addEventListener(type, fn, false);
else if (obj.attachEvent)
obj.attachEvent('on' + type, function() { return fn.apply(obj, [window.event]);});
}
and use like so
addEvent(links[i], 'click', [your function here]);
Using JQuery, the following code works:
function adaptLinks(table, sortableTable)
{
$('a[href]').click(function (e)
{
if(!e.isDefaultPrevented())
{
<do some work>
}
});
}
This requires using an extra library but avoids some issues that exist with addEventListener/attachEvent (like the latter's problem with this references).
There is just one pitfall: if the original onClick handler is assigned using "normal" JavaScript, the line
...
if(!e.isDefaultPrevented())
...
will always resolve to true, even in case the original handler canceled the event by returning false. To fix this, the original handler has to use JQuery as well.
This function should be usable (event listeners approach):
function addEventListener(element, eventType, eventHandler, useCapture) {
if (element.addEventListener) {
element.addEventListener(eventType, eventHandler, useCapture);
return true;
} else if (element.attachEvent) {
return element.attachEvent('on' + eventType, eventHandler);
}
element['on' + eventType] = eventHandler;
}
or you can save some more code adding this function (if you need to add the same event listener to many elements):
function addClickListener(element) {
addEventListener(element, 'click', clickHandler, false);
}
I had problems with overloading in the simple way - this page was a great resource
http://www.quirksmode.org/js/events_advanced.html

Categories

Resources