26

I would like to...

  1. Scan the document for all elements that have a certain class name
  2. Perform some key functions on the innerHTML of that element
  3. Change the class name of that element so that if I do another scan later I don't redo that element

I thought this code would work but for some reason it breaks the loop after the first instance and the element's class names are never changed. No frameworks please.

function example()
{
    var elementArray;

    elementArray = document.getElementsByClassName("exampleClass");

    for(var i = 0; i < elementArray.length; i++)
    {
        // PERFORM STUFF ON THE ELEMENT
        elementArray[i].setAttribute("class", "exampleClassComplete");
        alert(elementArray[i].className);
    }   
}

EDIT (FINAL ANSWER) -- Here is the final product and how I have implemented @cHao's solution within my site. The objective was to grab an assortment of timestamps on the page and change them to time ago. Thank you all for your help, I learned a ton from this question.

function setAllTimeAgos()
{
    var timestampArray = document.getElementsByClassName("timeAgo");

    for(var i = (timestampArray.length - 1); i >= 0; i--)
    {
        timestampArray[i].innerHTML = getTimeAgo(timestampArray[i].innerHTML);
        timestampArray[i].className = "timeAgoComplete";
    }
}
gmustudent
  • 2,229
  • 6
  • 31
  • 43
  • 16
    Why on earth does this question deserve -1 because the user doesn't want to use jquery or any other framework? +1 from me for the fact that they don't. – Xotic750 May 27 '13 at 18:38
  • When you want to build a plugin for external website, it is not safe to use jquery, it can alter the other page script – Jayo2k Jun 03 '15 at 13:13
  • possible duplicate of [How can I replace one class with another on all elements, using just the DOM?](http://stackoverflow.com/questions/16510973/how-can-i-replace-one-class-with-another-on-all-elements-using-just-the-dom) – Anonymous Jun 18 '15 at 15:08
  • Please don't answer the question in the question. Add a new answer with the answer. – theMayer Aug 02 '18 at 12:02

5 Answers5

15

The problem is that the NodeList returned to you is "live" - it changes as you alter the class name. That is, when you change the class on the first element, the list is immediately one element shorter than it was.

Try this:

  while (elementArray.length) {
    elementArray[0].className = "exampleClassComplete";
  }

(There's no need to use setAttribute() to set the "class" value - just update the "className" property. Using setAttribute() in old versions of IE wouldn't work anyway.)

Alternatively, convert your NodeList to a plain array, and then use your indexed iteration:

  elementArray = [].slice.call(elementArray, 0);
  for (var i = 0; i < elementArray.length; ++i)
    elementArray[i].className = "whatever";

As pointed out in a comment, this has the advantage of not relying on the semantics of NodeList objects. (Note also, thanks again to a comment, that if you need this to work in older versions of Internet Explorer, you'd have to write an explicit loop to copy element references from the NodeList to an array.)

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 1
    I wouldn't suggest `while (elementArray.length) {` as a condition. It's not like an Array where it just reads the cached property. Every fetch of `.length` needs to look a the DOM to see how many matched elements there are. Also, if a shim is used for `gEBCN` in IE8, you'll have an infinite loop because the list won't be live. –  May 27 '13 at 17:32
  • So the second I change the class name the element is taken from the list. That's insanity! Will accept when time limit is reached. Thank you for the assistance. – gmustudent May 27 '13 at 17:33
  • Not to nitpick, but your `.slice()` will fail in IE8 and lower because `elementArray` is a host object. IMO, I'd just do a reverse iteration. It'll work the same for live and non-live lists, and will be browser compatible and fast. –  May 27 '13 at 17:37
  • @squint I don't believe that's correct, but I'll try in IE8 to be sure. The call to the Array prototype "slice()" should work fine in all browsers. – Pointy May 27 '13 at 17:39
  • @Pointy: I agree that it should... but unfortunately IE8 and lower demanded native objects as the "context" of its native methods. Still let me know what you find out. I don't mind eating a little crow if I'm wrong. ;-) –  May 27 '13 at 17:40
  • 1
    @squint yes, you're right! Wow I'm glad I don't have to mess with IE very often nowadays :-) – Pointy May 27 '13 at 17:42
  • If supported by the browser you could use ´document.querySelectorAll´ as it returns a static node list, and should support IE8 for a simple class selector: https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll – Xotic750 May 27 '13 at 17:42
  • @Xotic750 yes that's yet another way to do it. Personally I'd do it with jQuery but I'm lazy :-) – Pointy May 27 '13 at 17:44
  • Could you briefly explain the slice call thing please? – gmustudent May 27 '13 at 17:45
  • @gmustudent well the idea is to use the "slice" code as a shortcut to a simple array copy loop. You can [check the MDN docs](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/call) to get an idea of how "call" works. In this case, the problem is that IE8 is really dumb. – Pointy May 27 '13 at 17:48
  • Another thing to note is that 'document.getElementsByClassName' isn't supported by IE8: https://developer.mozilla.org/en-US/docs/Web/API/document.getElementsByClassName – Xotic750 May 27 '13 at 17:50
  • I guess I should acknowledge one bit of reality that I didn't previously. That is that if indeed `gEBCN` is being used in IE8, it could only be because it was shimmed, and is therefore returning a native Array, which means that `[].slice.call(...` will work because it'll be operating on a native object. –  May 27 '13 at 17:50
  • Could something like this be okay in most browsers? `staticArray = Array.prototype.slice.call(elementArray);` – gmustudent May 27 '13 at 17:52
  • 1
    @gmustudent: That's no different. It's just a different path to the same method. –  May 27 '13 at 17:53
  • Ah so the Array class call at the begining is the same as []? Just making sure. Thank you! – gmustudent May 27 '13 at 17:54
  • @gmustudent: `[]` is an empty array object; Pointy is invoking the function `slice` on that object. The alternative `Array.prototype.slice` nicks that function directly from the definition of Arrays, without creating an instance of Array first. – Lightness Races in Orbit May 27 '13 at 18:20
  • Mind removing the "Using setAttribute() in old versions of IE wouldn't work anyway.)" part? IE8 actually supports get/setAttribute, but doesn't support getElementsByClassName so it's kind of a moot point and doesn't add much to the solution. – lqc May 27 '13 at 18:28
  • @lqc the comment was meant as a general admonition. In this case, you're right, but it always kind-of confuses and bothers me when I see code to modify DOM element properties via `setAttribute()`. :-) – Pointy May 27 '13 at 20:44
8

Most DOM functions that return a list of elements, return a NodeList rather than an array. The biggest difference is that a NodeList is typically live, meaning that changing the document can cause nodes to magically appear or disappear. That can cause nodes to move around a bit, and throw off a loop that doesn't account for it.

Rather than turning the list into an array or whatever, though, you could simply loop backwards on the list you get back.

function example()
{
    var elements = document.getElementsByClassName("exampleClass");

    for(var i = elements.length - 1; i >= 0; --i)
    {
        // PERFORM STUFF ON THE ELEMENT
        elements[i].className = "exampleClassComplete";

        // elements[i] no longer exists past this point, in most browsers
    }   
}

The liveness of a NodeList won't matter at that point, since the only elements removed from it will be the ones after the one you're currently on. The nodes that appear before it won't be affected.

cHao
  • 84,970
  • 20
  • 145
  • 172
  • +1 for reverse looping, as mentioned in the comments of @Pointy answer – Xotic750 May 27 '13 at 18:45
  • @Xotic750: Seemed the natural solution to me when i ran into this problem before. When i looked over the answers and noticed that most everyone was farting around with arrays...i had to chime in. :) – cHao May 27 '13 at 18:50
  • Yes, a very simple solution to making changes to a live node list where your change could affect the list :) – Xotic750 May 27 '13 at 18:52
1

Another approach could be to use a selector:

var arr = document.querySelectorAll('.exampleClass');
for (var i=0;i<arr.length;i++) {
    arr.innerHTML = "new value";
}

Although it's not compatible with older browsers, it can do the trick as well if the webcontent is for modern browsers.

Frederik.L
  • 5,522
  • 2
  • 29
  • 41
0

You can also, use 2 arrays, push data into the first array, then do what you need to do [like changing element's class]:

function example()
{
    var elementArray=[];
    var elementsToBeChanged=[];
    var i=0;

    elementArray = document.getElementsByClassName("exampleClass");

    for( i = 0; i < elementArray.length; i++){
        elementsToBeChanged.push(elementArray[i]);
    }

    for( i=0; i< elementsToBeChanged.length; i++)
    {
        elementsToBeChanged[i].setAttribute("class", "exampleClassComplete");
    }
}
0

Another possibility, which should also be cross-browser, use a hand taylored getElementsByClassName which returns a fixed node list as an array. This should support from IE5.5 and upwards.

function getElementsByClassName(node, className) {
    var array = [],
        regex = new RegExp("(^| )" + className + "( |$)"),
        elements = node.getElementsByTagName("*"),
        length = elements.length,
        i = 0,
        element;

    while (i < length) {
        element = elements[i];
        if (regex.test(element.className)) {
            array.push(element);
        }

        i += 1;
    }

    return array;
}
Xotic750
  • 22,914
  • 8
  • 57
  • 79