How do I use .forEach on DOM Elements?

Jess Lee on June 08, 2018

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 o... [Read Full]
markdown guide
 

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

 
 

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 😞

 

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));
 

I think you can also do:

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

Haven't tested it though 😊

 

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?

 

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.

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

 

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 😁

 

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.

 

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');
 

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.

 

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.

 

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.

 

Hi @jess and anyone who may enter for the foreach on dom elements, or NodeList Iteration class.
I recommend a gist that I found using a lot in my current web Proyect. Iterate over a NodeList

 

Why don't you just implement it by yourself?🤔 Something like this:

NodeList.prototype.foreach = function(cb, thisArg) {
   for(element of this)
     cb.call(thisArg, element);
}
 

This isn't related to the purpose of your post, but why are you creating a new variable (localTime), and is there a reason you're using a span instead of the time element?

 
 

I've always used [].forEach.call(nodeList, n => n.something), however be careful if in that loop you are removing or adding nodes. Found that out the hard way.

 

I always seem to forget that .forEach() only works on Arrays and not objects. Thank you

code of conduct - report abuse