11

I just want to change a classname to another one.

I've tried using

document.getElementsByClassName("current").setAttribute("class", "none");

but it doesn't work. I'm new at javascript.

Charles
  • 50,943
  • 13
  • 104
  • 142
Lohn Claidon
  • 405
  • 2
  • 7
  • 15
  • Possible duplicate of [What do querySelectorAll, getElementsByClassName and other getElementsBy\* methods return?](https://stackoverflow.com/questions/10693845/what-do-queryselectorall-getelementsbyclassname-and-other-getelementsby-method) – Sebastian Simon Jul 08 '18 at 11:20

2 Answers2

23

Explanation

document.getElementsByClassName returns an HTMLCollection, not just an array of elements. That means that the collection is live, so in this specific situation, it retains the requirement that it will always hold all elements with the class "current".

Coincidentally, you are removing the very class that the collection depends on, therefore updating the collection. It would be totally different if you were setting the value property (for example) in the loop - the collection wouldn't be affected, because the class "current" wasn't removed. It would also be totally different if you were adding a class, such as el.className += " none";, but that isn't the case.

A great description from the MDN docs:

HTMLCollections in the HTML DOM are live; they are automatically updated when the underlying document is changed.

Approach 1

An easy way to overcome all this pandemonium is by looping backwards.

var els = document.getElementsByClassName('current'),
    i = els.length;

while (i--) {
    els[i].className = 'none';
}

DEMO: http://jsfiddle.net/fAJgT/

(the code in the demo has a setTimeout, simply so you can see the original border color at first, then after 1.5 seconds, see it change)

This works because it modifies the last item in the collection - when it is modified (and automatically removed), move onto the item before it. So it doesn't suffer any consequences of the automatic removal.

An alternate setup, doing the same thing, is:

for (i = els.length; i >= 0; i--) {

Approach 2

Another answer made me realize you could just continually operate on the first item found. When you remove the specific class, the element is removed from the collection, so you're guaranteed that the first item is always a fresh item in the collection. Therefore, checking the length property should be a safe condition to check. Here's an example:

var els = document.getElementsByClassName('current');
while (els.length) {
    els[0].className = 'none';
}

DEMO: http://jsfiddle.net/EJLXe/

This is basically saying "while there's still items in the collection, modify the first one (which will be removed after modified)". I really wouldn't recommend ever using that method though, because it only works specifically because you end up modifying the collection. This would infinitely loop if you were not removing the specific class, or with a normal array or a non-live collection (without spliceing).

Approach 3

Another option is to slice (shallow copy) the collection into an array and loop through that normally. But I don't see any reason/improvement to do that. Here's an example anyways:

var els = document.getElementsByClassName('current'),
    sliced = Array.prototype.slice.call(els), i;

for (i = 0; i < sliced.length; i++) {
    sliced[i].className = 'none';
}

DEMO: http://jsfiddle.net/LHe95/2/

Approach 4

Finally, you could use document.querySelector - it returns a non-live NodeList (therefore you can loop like normal), and even has better support in browsers than document.getElementsByClassName does. Here's an example:

var els = document.querySelectorAll('.current'),
    i;

for (i = 0; i < els.length; i++) {
    els[i].className = 'none';
}

DEMO: http://jsfiddle.net/xq8Xr/


References:

Ian
  • 50,146
  • 13
  • 101
  • 111
  • @plalx I actually didn't downvote yours - but I didn't upvote either – Ian May 12 '13 at 19:13
  • I still don't see the purspose of iterating in reverse. – Austin Brunkhorst May 12 '13 at 19:13
  • 1
    +1, this is a valid point, otherwise it will only change every other element! – adeneo May 12 '13 at 19:14
  • @adeneo **and** go past the upper bound of the array and throw an error, I think - since you can't modify a property of `undefined` – Ian May 12 '13 at 19:15
  • Yup, every other element, does'nt look like it throws an error. – adeneo May 12 '13 at 19:17
  • Can anyone at least counter-balance the downvote on mine? It's simply ridiculous... – plalx May 12 '13 at 19:17
  • 1
    Huh, never realized this. +1 – Austin Brunkhorst May 12 '13 at 19:17
  • @plalx - would'nt it be better if they counter-balanced mine, as it actually works ? – adeneo May 12 '13 at 19:19
  • @AustinBrunkhorst It's specifically because `getElementsByClassName` returns an `HTMLCollection` and not an array – Ian May 12 '13 at 19:19
  • 2
    I guess most of us never think much about this, as we just do `$('.current').addClass('someClass');` – adeneo May 12 '13 at 19:22
  • @adeneo Haha I know! This problem only happens because we're specifically finding elements by class name, then modifying that class name. If we were modifying the id or adding an event handler, the collection wouldn't be modified and we wouldn't have this problem. – Ian May 12 '13 at 19:25
  • @Ian it also won't happen since we are using jQuery – John Dvorak May 12 '13 at 19:26
  • @JanDvorak Oh I know, I just meant with the OP's setup :) – Ian May 12 '13 at 19:27
  • @adeneo I realized it will go past the upper bound of the array only if you store the length of the array at the beginning and loop to that. If you check it every iteration, like Juhana's code, it will just hit every other item like you said. For example - http://jsfiddle.net/4JJP7/3/ – Ian May 12 '13 at 19:46
3

document.getElementsByClassName("current") returns a HTMLCollection, not an HTMLElement. You can access elements in the list the same way you would access an array.

The following will change the class of the first element in the HTMLCollection.

document.getElementsByClassName("current")[0].setAttribute("class", "none");

However, you do not have to use the setAttribute method, you can just set the className property of the element.

 element.className = 'none';

Please note that HTMLCollection are live, wich means that as soon as the element will not have the current class name anymore, the element will be removed from the array, so to avoid any issues you could iterate over it using the following approach:

var list = document.getElementsByClassName("current");

while (list.length) {
    list[0].className = 'none';
}
plalx
  • 42,889
  • 6
  • 74
  • 90
  • Another option with your new looping is to do `while (list.length) { list[0].className = 'none'; }` – Ian May 12 '13 at 19:27
  • What's the point of iterating if you select the same element on every iteration ? – adeneo May 12 '13 at 19:36
  • @adeneo The iterating just makes sure it iterates the correct number of times. If you always modify the first item in the collection, that one will always be removed from it. – Ian May 12 '13 at 19:38
  • 1
    @Ian - Aha, I get it. You'll see the strangest things on SO! – adeneo May 12 '13 at 19:47