DEV Community

Jess Lee
Jess Lee Subscriber

Posted on • Edited on

How do I use .forEach on DOM Elements?

The title of this post is what I originally googled. Here's what got me there:

I was working on displaying local times for the event listings on dev.to/events (haven't made a PR yet). To do this, I added a class to all elements with a timestamp, like this:

<span class="utc-time"><%= event.starts_at %></span>

I wanted to grab all the timestamps on the page, loop through them, and update their innerHTML to reflect the local time. I usually use for statements when I need to loop stuff, but I decided to try the .forEach function.



var timestamps = document.getElementsByClassName("utc-time");

timestamps.forEach(function(timestamp) {
      localTime = updateLocalTime(timestamps[i].innerHTML);
      timestamps[i].innerHTML = localTime;
    });


Enter fullscreen mode Exit fullscreen mode

I got this error:

Uncaught TypeError: timestamps.forEach is not a function

Eventually, I realized that timestamps was not an array, it was a NodeList and at the top of mdn documentation, it clearly states:

Although NodeList is not an Array, it is possible to iterate on it using forEach(). It can also be converted to an Array using Array.from().

However some older browsers have not yet implemented NodeList.forEach() nor Array.from(). But those limitations can be circumvented by using Array.prototype.forEach() (more in this document).

I probably should have googled "How to loop through a NodeList" for specificity. Anyway, so then I wrote this:



    Array.prototype.forEach.call(timestamps, function (timestamp) {
      localTime = updateLocalTime(timestamp.innerHTML);
      timestamp.innerHTML = localTime;
    });


Enter fullscreen mode Exit fullscreen mode

And it worked! But when I showed it to @maestromac, he told me that a simple for statement would have worked. And would probably be a bit safer. So I went back to what I was most familiar with:



    for (var i = 0; i < timestamps.length; i++) {
      localTime = updateLocalTime(timestamps[i].innerHTML);
      timestamps[i].innerHTML = localTime
    }


Enter fullscreen mode Exit fullscreen mode

At least I learned something about NodeLists today Β―_(ツ)_/Β―

Top comments (25)

Collapse
 
ben profile image
Ben Halpern • Edited

Ah this is a classic rites of passage in DOM unintutiveness.

Collapse
 
nektro profile image
Meghan (she/her)

If only NodeList inherited from Array...

Collapse
 
maxart2501 profile image
Massimo Artizzu

There are some methods that don't make much sense in relation to NodeLists. Mostly mutating methods like push, sort or splice.

The others are fine, though. I'd love to have map or filter 😞

Collapse
 
adityavm profile image
Aditya Mukherjee

use the "…" spread operator, which works for both getElementsByClassName() and querySelectorAll() so you can port easily later.

// example to run on this page
[...document.getElementsByClassName("stories")].forEach(node => console.log(node));
Collapse
 
wilburpowery profile image
Wilbur Powery

I think you can also do:

var timestamps = Array.from(document.getElementsByClassName("utc-time"));

Haven't tested it though 😊

Collapse
 
jess profile image
Jess Lee

Well, in the mdn documentation it had mentioned this:

However some older browsers have not yet implemented NodeList.forEach() nor Array.from()

So I assumed that if forEach wasn't supported, Array.from wouldn't be either?

Collapse
 
wilburpowery profile image
Wilbur Powery

I just tested it and it works on Chrome for me.

var elements = Array.from(document.querySelectorAll('p'));

// elements = (3)Β [p, p, p]

It might not be compatible with many browsers though, as you mentioned.

Thread Thread
 
maxart2501 profile image
Massimo Artizzu

You don't have to go far - if you have Windows and IE11 installed, that doesn't support either πŸ˜‰

Thread Thread
 
notriddle profile image
Michael "notriddle" Howell

The MDN documentation also has an Array.from polyfill:

developer.mozilla.org/en-US/docs/W...

Collapse
 
maxart2501 profile image
Massimo Artizzu • Edited

A small correction: you used document.getElementsByClassName which does not return a NodeList but a HTMLCollection. Now, the former does have forEach defined - but it's pretty much the only array method that has been added to its prototype so far.

But it's only a relatively recent addition, so older browsers don't support it - fortunately, the Array#forEach trick works pretty well, down to sufficiently old Internet Explorer versions (probably 6? 5.5? The heck am I saying, that could work for slice, but forEach was added only in IE9...).

A HTMLCollection is a totally different beast... and something that should be avoided in general. It's a live collection that gets updated when the DOM changes. Quite heavy when it comes to memory consumption and CPU usage.

Conclusion: use document.querySelectorAll instead (which returns a NodeList).

... a simple for statement would have worked. And would probably be a bit safer.

I don't agree with that Β―\_(ツ)_/Β―
It's true that every browser supports for (duh!), but experience proved that something that iterates over a collection for us is simpler as it doesn't force us to take care of a variable for counting, while the (relatively) complex - although well-known - syntax of for is prone to mistakes.

Mostly caused by distraction and/or boredom 😁

Collapse
 
learosema profile image
Lea Rosema (she/her) • Edited

A best practice is to convert a NodeList to a normal Array, so you can use the built-in forEach/map functions.

One ES5 way is to create a helper function, for example on the NodeList prototype:

NodeList.prototype.toArray = function() { 
  return Array.prototype.slice.call(this);
}

// So you could call
document.querySelectorAll('div').toArray().forEach(function (el) {
  el.style.color = 'pink';
})

Another popular way is to create a jQuery-ish helper function that wraps the conversion to array. This is also used in Lea Verou's bliss.js library:

function $$(sel, con) {
  return Array.prototype.slice.call((con||document).querySelectorAll(sel));
}

The modern ES7+ way is to use the ... operator:

const divs = [...document.querySelectorAll('div')];
divs.forEach(el => el.style.color = 'pink');
Collapse
 
litedev profile image
lite-dev • Edited

I just want to make a few points.

  1. I haven't used a traditional for loop in years.
  2. Like others have mentioned, you should be using more modern functions such as querySelectorAll.
  3. getElementsByClassName returns an HTMLCollection, which is why you were getting the error. NodeLists have a forEach method and HTMLCollections do not.

My initial instinct on circumventing this is the following:

var timestamps = document.getElementsByClassName("utc-time");

[].slice.call(timestamps).forEach(function(timestamp) {
      var localTime       = updateLocalTime(timestamp.innerHTML);
      timestamp.innerHTML = localTime;
});

HTMLCollections are array-like. They are iterable and have a length property. The slice function can convert array-like objects into Arrays. this is bound to our array-like object. slice iterates over this using the length property since no other arguments were given. All the elements are returned in a new Array then we can call forEach method on our Array

But looking at your initial answer:

var timestamps = document.getElementsByClassName("utc-time");

[].forEach.call(timestamps, function(timestamp) {
      var localTime       = updateLocalTime(timestamp.innerHTML);
      timestamp.innerHTML = localTime;
});

Is a good solution too if not better. timestamps is bound to this then forEach method iterates over the array-like object by using the length property. My solution would have looped through the array-like object once then the newly created array versus this solution which is once.

Collapse
 
modayilme profile image
Benjamin Modayil πŸ‘¨β€πŸ’» • Edited

Yep. Wish we could use forEach on arrays. At the moment, I just convert them over to arrays by using the spread operator:

// return a node-list
const domItems = document.querySelectorAll('.dom-items')

// takes all those node-list items and dumps them in an array
const domItems = [...document.querySelectorAll('.dom-items')]

At first, I would just use the forEach and continue to treat it as a node-list unless but I needed it in an array form, but Babel apparently doesn't convert forEach to IE11 friendly syntax. So now, I just convert node-lists to arrays and then go on from there unless I add a specific IE11 polyfill.

Collapse
 
ycmjason profile image
YCM Jason • Edited

Wait, is this true?

Eventually, I realized that timestamps was not an array, it was a NodeList and at the top of mdn documentation, ...

I seldom use selectElementsByClassName, but according to mdn it returns a HTMLCollection. And the mdn doc clearly says it hasn't got forEach.

But for NodeList, you should be able to do forEach. If you do document.querySelectorAll('.className'), you will get an NodeList and you should be able to do forEach. See here.

Howeverrrrr, since most older browsers won't have NodeList.prototype.forEach defined, it is probably safer to do what you suggested Array.prototype.forEach.call(elements, ...) or just [].forEach.call(elements, ...). A more "es6" way would probably be Array.from(elements).forEach(...).

Orrrrr, you could do it with for-loops as you suggested. "es6" introduced this amazing for-of loop, it could loop through most list-like things. So the following would work as well.

for (const el of elements) {
  // ...
}

Of course, to safely use ES6 features you probably want your polyfills + babel set up to support old browsers.

Collapse
 
alainvanhout profile image
Alain Van Hout

Although it's a very good point that you can use a for-loop in this case, your own solution can be simplified to [].forEach.call, which would also work in case you'd need other array operations such as map, filter or reduce.