342

I'm currently trying to write some JavaScript to get the attribute of the class that has been clicked. I know that to do this the correct way, I should use an event listener. My code is as follows:

var classname = document.getElementsByClassName("classname");

var myFunction = function() {
    var attribute = this.getAttribute("data-myattribute");
    alert(attribute);
};

classname.addEventListener('click', myFunction(), false);

I was expecting to get an alert box every time I clicked on one of the classes to tell me the attribute but unfortunately this does not work. Can anyone help please?

(Note - I can quite easily do this in jQuery but I would NOT like to use it)

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
30secondstosam
  • 4,476
  • 4
  • 28
  • 33
  • 3
    There's a problem with the code that's adding the event listener. addEventListener takes the event name ('click'), reference to the function (not the result of the function as it is now by calling myFunction() with parens) and a flag to indicate event bubbling. The addEventListener call should look like: elem.addEventListener('click', myFunction, false) and classname is a NodeList type. Need to loop over all the elements and attach the listener to each one in the list. – Joey Guerra Mar 23 '15 at 03:03

8 Answers8

591

This should work. getElementsByClassName returns an Array-like object (see below) of the elements matching the criteria.

var elements = document.getElementsByClassName("classname");

var myFunction = function() {
    var attribute = this.getAttribute("data-myattribute");
    alert(attribute);
};

for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', myFunction, false);
}

jQuery does the looping part for you, which you need to do in plain JavaScript.

If you have ES6 support you can replace your last line with:

    Array.from(elements).forEach(function(element) {
      element.addEventListener('click', myFunction);
    });

Note: Older browsers (like IE6, IE7, IE8) don´t support getElementsByClassName and so they return undefined.


Details on getElementsByClassName

getElementsByClassName doesn't return an array, but a HTMLCollection in most, or a NodeList in some browsers (Mozilla ref). Both of these types are Array-Like, (meaning that they have a length property and the objects can be accessed via their index), but are not strictly an Array or inherited from an Array (meaning other methods that can be performed on an Array cannot be performed on these types).

Thanks to user @Nemo for pointing this out and having me dig in to fully understand.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
Anudeep Bulla
  • 8,318
  • 4
  • 22
  • 29
  • 11
    This works perfectly. Thank you. I actually didn't realise that jQuery did the looping. Great help Anudeep. Here's your working answer: http://jsfiddle.net/LWda3/2/ – 30secondstosam Oct 29 '13 at 10:33
  • 2
    `document.getElementsByClassName` actually always return an array, even if only one element matches the criteria – Guilherme Sehn Oct 29 '13 at 10:39
  • Careful though the first element of the array is all the dom elements. So start your for loop with 1 – Vishal Sakaria Sep 13 '15 at 17:22
  • 15
    http://stackoverflow.com/a/13258908/1333493 "document.getElementsByClassName does not return an array. It returns a node list which is traversed like an XML file." – Nemo Sep 24 '15 at 10:20
  • 11
    `Array.from()` has a second parameter which is a map function so the above (assuming es6 support) could be written `Array.from(classname, c => c.addEventListener('click', myFunction));`. – Kilmazing Jan 05 '17 at 20:34
  • What happens if the number of tags with the class changes? For example, one is deleted? Would manual cleanup of the event listener be required? – Justin Oroz Apr 21 '17 at 12:37
  • Please note that Array.from does not support on IE https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from – super1ha1 Nov 09 '17 at 09:50
  • What if elements from that class are constantly being added and removed? – Robert Moore Apr 24 '18 at 21:47
  • @RobertMoore see my answer, should help with that – obermillerk Jun 18 '18 at 00:09
  • 1
    Better attach event to a parent and delegates it in place to attach it to every single element. – Salvio Aug 02 '23 at 08:09
59

With modern JavaScript it can be done like this:

const divs = document.querySelectorAll('.a');

divs.forEach(el => el.addEventListener('click', event => {
  console.log(event.target.getAttribute("data-el"));
}));
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Example</title>
  <style>
    .a {
      background-color:red;
      height: 33px;
      display: flex;
      align-items: center;
      margin-bottom: 10px;
      cursor: pointer;
    }
    
    .b {
      background-color:#00AA00;
      height: 50px;
      display: flex;
      align-items: center;
      margin-bottom: 10px;
    }
  </style>
</head>
<body>
  <div class="a" data-el="1">1</div>
  <div class="b" data-el="no-click-handler">2</div>
  <div class="a" data-el="3">11</div>
</body>
</html>
  1. Gets all elements by class name
  2. Loops over all elements with using forEach
  3. Attach an event listener on each element
  4. Uses event.target to retrieve more information for specific element
V. Sambor
  • 12,361
  • 6
  • 46
  • 65
  • This is what I used, but I can't vote this answer as useful since you deviated away from what the function in the question is trying to do. You should really update your answer to pull the data attribute value rather than a class name. – Mark Jan 12 '21 at 16:02
  • @Mark Thank you :) I have updated as you required. Have a nice day! – V. Sambor Jan 12 '21 at 17:28
24

You can use the code below:

document.body.addEventListener('click', function (evt) {
    if (evt.target.className === 'databox') {
        alert(this)
    }
}, false);
James
  • 2,732
  • 2
  • 5
  • 28
Rajat kumar
  • 281
  • 2
  • 2
  • 1
    I'll have to give this a try, it's a unique approach which might actually be easier to maintain and perform better than looping! – Cliff Helsel Mar 02 '19 at 17:13
  • 9
    if the element has multiple classes, you would have to check for the correct value in that fields eg. classes and order. i'd rather go for `evt.target.classList.contains('databox')` – honk31 Mar 06 '19 at 10:50
  • 12
    This seems wildly inefficient to me. Every single event on the body would go through this function. What's so hard to maintain about a loop? – JosefAssad Jul 28 '19 at 18:20
  • 1
    This is very inefficient, you would go through each element as @JosefAssad said. – nullwriter Sep 02 '19 at 09:42
  • Not recommending this approach as just adding too much overhead to front-end – st35ly Dec 07 '20 at 04:49
  • 6
    I fully disagree with everybody here. If you use element.addEventListener('click', event => { } to add the event to the element, the javascript will check every click internally anyway. If you add the event to all element of classes Javascript will run equivalent check instances for every single click, while the above approach will only look once for every single click. – Thanasis Jan 06 '21 at 16:15
  • 1
    @honk31 dear friend. Actually the above method is more efficient than any answer here – Thanasis Jan 06 '21 at 16:16
  • I really like this approach when inefficiency is not a concern. – Gass Sep 22 '21 at 17:21
  • This should work really well, especially when elements of a class are created on the fly. – TheBoyne Nov 14 '21 at 14:40
  • 1
    @Thanasis I'm with you on this one. Event delegation is not inherently less efficient than direct assignment of listeners. The same event capturing and bubbling processes take place in either scenario. In fact, it can be argued that event delegation actually *increases* efficiency, since it avoids the violation of the DRY principle that assigning the same event handler to a collection of like elements necessarily entails. – BobRodes Aug 23 '22 at 14:40
  • 2
    @GabrielSalinasSzada I really like this approach when inefficiency *is* a concern, personally. – BobRodes Aug 23 '22 at 14:40
23

* This was edited to allow for children of the target class to trigger the events. See bottom of the answer for details. *

An alternative answer to add an event listener to a class where items are frequently being added and removed. This is inspired by jQuery's on function where you can pass in a selector for a child element that the event is listening on.

var base = document.querySelector('#base'); // the container for the variable content
var selector = '.card'; // any css selector for children

base.addEventListener('click', function(event) {
  // find the closest parent of the event target that
  // matches the selector
  var closest = event.target.closest(selector);
  if (closest && base.contains(closest)) {
    // handle class event
  }
});

Fiddle: https://jsfiddle.net/u6oje7af/94/

This will listen for clicks on children of the base element and if the target of a click has a parent matching the selector, the class event will be handled. You can add and remove elements as you like without having to add more click listeners to the individual elements. This will catch them all even for elements added after this listener was added, just like the jQuery functionality (which I imagine is somewhat similar under the hood).

This depends on the events propagating, so if you stopPropagation on the event somewhere else, this may not work. Also, the closest function has some compatibility issues with IE apparently (what doesn't?).

This could be made into a function if you need to do this type of action listening repeatedly, like

function addChildEventListener(base, eventName, selector, handler) {
  base.addEventListener(eventName, function(event) {
    var closest = event.target.closest(selector);
    if (closest && base.contains(closest)) {
      // passes the event to the handler and sets `this`
      // in the handler as the closest parent matching the
      // selector from the target element of the event
      handler.call(closest, event);
    }
  });
}

=========================================
EDIT: This post originally used the matches function for DOM elements on the event target, but this restricted the targets of events to the direct class only. It has been updated to use the closest function instead, allowing for events on children of the desired class to trigger the events as well. The original matches code can be found at the original fiddle: https://jsfiddle.net/u6oje7af/23/

obermillerk
  • 1,560
  • 2
  • 11
  • 12
4

Yow can use querySelectorAll to select all the classes and loop through them to assign the eventListener. The if condition checks if it contains the class name.

const arrClass = document.querySelectorAll(".className");
for (let i of arrClass) {
  i.addEventListener("click", (e) => {
    if (e.target.classList.contains("className")) {
        console.log("Perfrom Action")
    }
  })
}
Bhargav
  • 71
  • 3
  • 1
    Unless you suspect the className to dynamically change, you shouldn't have to recheck the className of the even target. – CopyJosh Dec 01 '20 at 20:34
0

Also consider that if you click a button, the target of the event listener is not necessaily the button itself, but whatever content inside the button you clicked on. You can reference the element to which you assigned the listener using the currentTarget property. Here is a pretty solution in modern ES using a single statement:

    document.querySelectorAll(".myClassName").forEach(i => i.addEventListener(
        "click",
        e => {
            alert(e.currentTarget.dataset.myDataContent);
        }));
davidthegrey
  • 1,205
  • 2
  • 14
  • 23
0

All the above-mentioned answers are correct and I just want to demonstrate another short way

document.querySelectorAll('.classname').forEach( button => {
    button.onclick = function () {
    // rest of code
    }
});
Elyas Hadizadeh
  • 3,289
  • 8
  • 40
  • 54
0

Here's a different approach for many DOM elements with the same class name by selecting path key in the eventListener object.

Add an event listener to the immediate parent class wrapping all the child elements with the same class and get the path by selecting the first key in the event object.

E.g say you want to edit table cells of a table

    //Select tbody element & add  event listener
    let tbody = document.querySelector('tbody');

    tbody.addEventListener("click", function(e,v) {
    // Get the clicked cell
    let cell = e.path[0];
    // Get the current cell value
    let cellValue = cell.innerHTML;
    //Rest of code goes here
    }