36

I am writing some vanilla JavaScript to create a nice navigation menu. I am stuck on adding an active class.

I am getting elements by class name NOT by id. The below code works if substituted with id, however, I need it to apply to more than one element.

HTML

<img class="navButton" id="topArrow" src="images/arrows/top.png" />
<img class="navButton" id="rightArrow" src="images/arrows/right.png" />

JS

var button = document.getElementsByClassName("navButton");

button.onmouseover = function() {
    button.setAttribute("class", "active");
    button.setAttribute("src", "images/arrows/top_o.png");
}

No answers containing jQuery please.

Brett DeWoody
  • 59,771
  • 29
  • 135
  • 184
vincentieo
  • 940
  • 2
  • 13
  • 28
  • duplicate of [getElementByClass().setAttribute doesn't work](http://stackoverflow.com/questions/2565909/getelementbyclass-setattribute-doesnt-work) and [getElementsByClassName onclick issue](http://stackoverflow.com/questions/13667533/getelementsbyclassname-onclick-issue) – Bergi Jul 30 '13 at 10:48
  • Sidenote, if you want to target IE8 then document.getElementsByClassName is not available, but document.querySelectorAll is. They both return a NodeList. https://developer.mozilla.org/en-US/docs/Web/API/document.querySelectorAll and https://developer.mozilla.org/en-US/docs/Web/API/document.getElementsByClassName – ivarni Jul 30 '13 at 10:56
  • 1
    Oof, my memory was a bit off, getElementsByClassName apparently returns a HTMLCollection. – ivarni Jul 30 '13 at 10:58

7 Answers7

45

document.getElementsByClassName returns a node list. So you'll have to iterate over the list and bind the event to individual elements. Like this...

var buttons = document.getElementsByClassName("navButton");

for(var i = 0; i < buttons.length; ++i){
    buttons[i].onmouseover = function() {
        this.setAttribute("class", "active");
        this.setAttribute("src", "images/arrows/top_o.png");
    }
}
mohkhan
  • 11,925
  • 2
  • 24
  • 27
  • Thanks, that looks like a good solution...will let you know how I get on with it. – vincentieo Jul 30 '13 at 10:55
  • @vincentieo: Please, if the client moves the mouse over all buttons, they'll all show the same picture, and they'll all have the `active` class. You're attaching tons of event listeners all over the place, and if ever you're going to add a new navigation element using ajax, this won't work. Honestly: _delegate the event_. It's, by far, more efficient, flexible, and most JS-like approach to this problem. Check the fiddle at the bottom of my answer: 1 event listener, no global variables, no non-sense – Elias Van Ootegem Jul 30 '13 at 11:11
  • 1
    @vincentieo: would you be so kind as to explain _why_ you prefer this to delegation? I'm honestly struggeling to make sense of your choice here – Elias Van Ootegem Jul 30 '13 at 11:14
  • @EliasVanOotegem perhaps this solution seems more succint and short, thus easy to understand at first sight for beginners (Such in my case). But I ll give yours a try ;) – Yannis Dran Jan 23 '14 at 20:23
  • 1
    In case if someone else is reading the OP question, you could use .classList.add("active"); and to remove you do the opposite. .remove("active"); – dragonore May 16 '15 at 21:04
7

In your snippet, button is an instance of NodeList, to which you can't attach an event listener directly, nor can you change the elements' className properties directly.
Your best bet is to delegate the event:

document.body.addEventListener('mouseover',function(e)
{
    e = e || window.event;
    var target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() === 'img' && target.className.match(/\bnavButton\b/))
    {
        target.className += ' active';//set class
    }
},false);

Of course, my guess is that the active class needs to be removed once the mouseout event fires, you might consider using a second delegator for that, but you could just aswell attach an event handler to the one element that has the active class:

document.body.addEventListener('mouseover',function(e)
{
    e = e || window.event;
    var oldSrc, target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() === 'img' && target.className.match(/\bnavButton\b/))
    {
        target.className += ' active';//set class
        oldSrc = target.getAttribute('src');
        target.setAttribute('src', 'images/arrows/top_o.png');
        target.onmouseout = function()
        {
            target.onmouseout = null;//remove this event handler, we don't need it anymore
            target.className = target.className.replace(/\bactive\b/,'').trim();
            target.setAttribute('src', oldSrc);
        };
    }
},false);

There is some room for improvements, with this code, but I'm not going to have all the fun here ;-).

Check the fiddle here

Community
  • 1
  • 1
Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
  • how to use it for document ready instead of mouseover listener case? – Yannis Dran Jan 23 '14 at 21:13
  • 1
    @YannisDran: either use `document.addEventListener('readystatechange', function(){ if (document.readyState === 'complete') console.log('ready');}, false);` or, which is what I think you actually want: `window.addEventListener('load', function(){ console.log('window loaded');}, false);` Both work equally well. Also check [my answer to this question](http://stackoverflow.com/questions/11186750/memory-leak-risk-in-javascript-closures), which explains why you'd want to use an event listener for `window.onload` instead of binding a handler directly... – Elias Van Ootegem Jan 24 '14 at 07:37
  • Thanks. Actually, I wanted add a class and I tried to find how I could attach a document ready event so that the rest of the code would be executed correctly. Check my relevant question to understand what I meant: http://stackoverflow.com/questions/21319503/select-and-add-class-in-javascript – Yannis Dran Jan 24 '14 at 12:01
2

Here is a method adapted from Jquery 2.1.1 that take a dom element instead of a jquery object (so jquery is not needed). Includes type checks and regex expressions:

function addClass(element, value) {
    // Regex terms
    var rclass = /[\t\r\n\f]/g,
        rnotwhite = (/\S+/g);

    var classes,
        cur,
        curClass,
        finalValue,
        proceed = typeof value === "string" && value;

    if (!proceed) return element;

    classes = (value || "").match(rnotwhite) || [];

    cur = element.nodeType === 1
        && (element.className
                ? (" " + element.className + " ").replace(rclass, " ")
                : " "
        );

    if (!cur) return element;

    var j = 0;

    while ((curClass = classes[j++])) {

        if (cur.indexOf(" " + curClass + " ") < 0) {

            cur += curClass + " ";

        }

    }

    // only assign if different to avoid unneeded rendering.
    finalValue = cur.trim();

    if (element.className !== finalValue) {

        element.className = finalValue;

    }

    return element;
};
Aleksandr Albert
  • 1,777
  • 1
  • 19
  • 26
0

getElementsByClassName() returns HTMLCollection so you could try this

var button = document.getElementsByClassName("navButton")[0];

Edit

var buttons = document.getElementsByClassName("navButton");
for(i=0;buttons.length;i++){
   buttons[i].onmouseover = function(){
     this.className += ' active' //add class
     this.setAttribute("src", "images/arrows/top_o.png");
   }
}
Arda
  • 470
  • 5
  • 10
  • 1
    NodeList, not an array. – Quentin Jul 30 '13 at 10:48
  • @ArdaChapuler: OP needs to add the class to _all_ elements in the list, not just the first – Elias Van Ootegem Jul 30 '13 at 10:51
  • That works for the first image but not for the second (or third or forth). Is there some kind of wildcard to be used? I cant believe that there is no way to add a class to an element, on mouse over, according to elements in a class. Perhaps I get an containing div id and look for elements inside it to add class? – vincentieo Jul 30 '13 at 10:53
  • do you want to add class or set class? – Arda Jul 30 '13 at 10:54
  • 1
    @vincentieo: That's because `button` is being assigned a reference to the first element, and no other. The suggestion to solve this with a loop is attaching tons of event listeners, which is wasteful. Check my answer, which uses but 1 listener to delegate the event... – Elias Van Ootegem Jul 30 '13 at 11:01
  • yup.. +1 to Elias' answer. His solution is better. – Arda Jul 30 '13 at 11:27
  • @ArdaChapuler: Thanks for the +1. You wouldn't happen to know why the OP _isn't_ going for delegation, but is binding all events individually? The way the accepted answer looks, you just _know_ what the next question is going to be: _"How can I restore the `src` attribute, and how can I _unset_ the `active` class?"_ or something... I just don't get it why he's not using delegation... – Elias Van Ootegem Jul 30 '13 at 12:07
  • people likes quick and dirty ways :) – Arda Jul 30 '13 at 12:30
0

There is build in forEach loop for array in ECMAScript 5th Edition.

var buttons = document.getElementsByClassName("navButton");

Array.prototype.forEach.call(buttons,function(button) { 
    button.setAttribute("class", "active");
    button.setAttribute("src", "images/arrows/top_o.png"); 
});
ketan
  • 390
  • 3
  • 6
0

I like to use a custom "foreach" function of sorts for these kinds of things:

function Each( objs, func )
{
    if ( objs.length ) for ( var i = 0, ol = objs.length, v = objs[ 0 ]; i < ol && func( v, i ) !== false; v = objs[ ++i ] );
    else for ( var p in objs ) if ( func( objs[ p ], p ) === false ) break;
}

(Can't remember where I found the above function, but it has been quite useful.)

Then after fetching your objects (to elements in this example) just do

Each( elements, function( element )
{
    element.addEventListener( "mouseover", function()
    {
        element.classList.add( "active" );
        //element.setAttribute( "class", "active" );
        element.setAttribute( "src", "newsource" );
    });

    // Remove class and new src after "mouseover" ends, if you wish.
    element.addEventListener( "mouseout", function()
    {
        element.classList.remove( "active" );
        element.setAttribute( "src", "originalsource" );
    });
});

classList is a simple way for handling elements' classes. Just needs a shim for a few browsers. If you must use setAttribute you must remember that whatever is set with it will overwrite the previous values.

EDIT: Forgot to mention that you need to use attachEvent instead of addEventListener on some IE versions. Test with if ( document.addEventListener ) {...}.

ojrask
  • 2,799
  • 1
  • 23
  • 23
0

Simply add a class name to the beginning of the funciton and the 2nd and 3rd arguments are optional and the magic is done for you!

function getElementsByClass(searchClass, node, tag) {

  var classElements = new Array();

  if (node == null)

    node = document;

  if (tag == null)

    tag = '*';

  var els = node.getElementsByTagName(tag);

  var elsLen = els.length;

  var pattern = new RegExp('(^|\\\\s)' + searchClass + '(\\\\s|$)');

  for (i = 0, j = 0; i < elsLen; i++) {

    if (pattern.test(els[i].className)) {

      classElements[j] = els[i];

      j++;

    }

  }

  return classElements;

}
JoniS
  • 89
  • 9
  • 11