loading...

How do I use .forEach on DOM Elements?

Jess Lee (she/her) 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 on ...
pic
Editor guide
Collapse
ben profile image
Ben Halpern

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 (she/her) Author

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

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
litedev profile image
lite-dev

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 👨‍💻

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
Jason Yu

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
terabaud profile image
Lea Rosema

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
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.

Collapse
burzumumbra profile image
Ronald Flores Sequeira

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

Collapse
maple3142 profile image
maple

developer.mozilla.org/en-US/docs/W...
Isn't NodeList.prototype.forEach exists?

Collapse
stanleynguyen profile image
Stanley Nguyen

Guess her browser hasn't implemented it

Collapse
moopet profile image
Ben Sinclair

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?

Collapse
pmcgowan profile image
p-mcgowan

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.

Collapse
pandax54 profile image
pandax54

And I've learned too, thanks =)

Collapse
jess profile image
Collapse
oathkeeper profile image
Divyesh Parmar

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

Collapse
briansanchez profile image
Brian Sanchez

this works, thank you!.

Collapse
jess profile image