20

I'm building an app with Babel/ES6. I want to disable all form elements for a view-only version of it, so I did this:

let form = document.getElementById('application-form')
let elements = form.elements

I expected to be able to do this, instead of using a regular old for loop (which did work):

elements.forEach((el) => {
    el.disabled = true
})

but I got TypeError: elements.forEach is not a function

The strange thing is if I console.log(elements) in the Chrome dev console it exactly like an array with a bunch of input objects. It doesn't display with the Object notation for objects, and all of the keys are integers. I'm thinking it's some sort of pseudo array, but I wouldn't even know how to find that out.

EDIT: short answer it's not an array it's an HTMLCollection. see Why doesn't nodelist have forEach?


*UPDATE*

Per this answer, nodelist now has the forEach method!

Community
  • 1
  • 1
inostia
  • 7,777
  • 3
  • 30
  • 33
  • And the result of `console.log(typeof elements)` – Hackerman Sep 30 '16 at 17:40
  • 3
    Fancy solution: `[...elements].forEach` – Sterling Archer Sep 30 '16 at 17:43
  • `object`. but it looked like an array in the dev console. looks like it is actually a HTMLCollection – inostia Sep 30 '16 at 17:43
  • Always check the type...do not use `duck testing`(If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck) – Hackerman Sep 30 '16 at 17:45
  • @squint: Ah, yes indeed. (I knew there had to be one, half the reason I CW'd -- the other half being the link to my arrays answer...) And since you say *you* reopened it...sure enough, I was able to dupehammer it (I don't think I could have if it had been me reopening). – T.J. Crowder Sep 30 '16 at 17:52
  • I don't think reopening prevents one from closing. I did that to you one time when my pedantry engine was in high gear. I didn't check to see if it was already reopened when I clicked, so it could be I was behind you. In any case, I agree that this is a decent CW candidate. –  Sep 30 '16 at 18:00
  • 1
    @squint: *"...when my pedantry engine was in high gear..."* ROFL! I'm sure I have **no** idea what that's like. – T.J. Crowder Sep 30 '16 at 18:01

3 Answers3

35

You can. You just can't use it like that, because there is no forEach property on the HTMLFormControlsCollection that form.elements gives you (which isn't an array).

Here's how you can use it anyway:

Array.prototype.forEach.call(form.elements, (element) => {
    // ...
});

Or on modern browsers you can make use of the fact that the underlying HTMLCollection is iterable even though it doesn't have forEach:

// `for-of`:
for (const element of elements) {
    // ...do something with `element`...
}

// Spreading into an array:
[...elements].forEach((element) => {
    // ...do something with `element`...
});

That won't work on obsolete browsers like IE11 though.

There's also Array.from (needs a polyfill for obsolete browsers):

Array.from(elements).forEach((element) => {
    // ...
});

For details, see the "array-like" part of my answer here.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 2
    ES6 conversion: `[...elements].forEach(el => el.disabled = true);` – Sterling Archer Sep 30 '16 at 17:44
  • @SterlingArcher: Indeed! Hmmm, I'm not sure my answer linked above mentions that... Edit: Ah, yes, it does. But it incorrectly calls it an operator. Edit2: Not any more. :-) – T.J. Crowder Sep 30 '16 at 17:45
  • Feel free to edit into solution, since ES6 is indeed tagged. `Array.from` is also good. – Sterling Archer Sep 30 '16 at 17:46
  • Using the `spread` is an awesome trick. – inostia Sep 30 '16 at 17:49
  • @inostia: Yeah, so succinct! – T.J. Crowder Sep 30 '16 at 17:49
  • 1
    @SterlingArcher: That and the `.from()` solution are fine, but they unnecessarily create a new Array and copies in the nodes. Too bad Firefox's Array generic methods weren't standardized. Nicer to do `Array.forEach(elements, function(el) {...})` –  Sep 30 '16 at 17:54
  • 2
    @T.J.Crowder actually that did not work for me. `[...elements].forEach` gave me an Unexpected Token error with WebPack. But `let elements = [...form.elements]; elements.forEach` did work. – inostia Sep 30 '16 at 17:55
  • @squint: Indeed. It's really well optimized, but still... – T.J. Crowder Sep 30 '16 at 17:55
  • @inostia: !!! Good catch, spread will only work with iterables! Not everything that `Array.from` can turn into an array is an iterable! Interestingly, the result of `querySelectorAll` appears to be iterable (on Chrome) and works fine with spread notation. – T.J. Crowder Sep 30 '16 at 17:57
  • @SterlingArcher: See inostia's note above, which I can verify. Oops! – T.J. Crowder Sep 30 '16 at 17:59
  • Are you sure? `[...document.querySelectorAll("div")].forEach(x => console.log(x));` runs in my console just fine. Unless HTMLCollection != NodeList? – Sterling Archer Sep 30 '16 at 18:01
  • @SterlingArcher: I can verify that not all array-like objects work with spread notation: https://jsfiddle.net/3p0jntco/ (of course, if we think about it). For me on Chrome, QSA's return value and `HTMLFormControlsCollection` both work with spread notation, but apparently webpack thinks not, which makes me wonder if there are implementations in the wild that also think not... Basically, they'd have to be iterable. It makes *sense* for them to be iterable, but... – T.J. Crowder Sep 30 '16 at 18:04
  • @T.J.Crowder like I mentioned, using the `forEach` directly on `[...elements]` failed, but I was able to set `[...form.elements]` to a variable and iterate over that with `forEach`. Maybe a webpack issue – inostia Sep 30 '16 at 18:10
  • @inostia: Sorry, misread that, of course you did say. But do note that you're relying on iterability there. Need to check the relevant specs to make sure that's guaranteed (well, you'll test in your target browsers anyway, and transpiling will do an `Array.from`-like thing anyway). – T.J. Crowder Sep 30 '16 at 18:13
  • Just to add an alternative, if you can use ES6, just go with `for (var el of elements) {}`. – Marc Apr 27 '18 at 02:53
  • @Marc: On modern browsers only, yeah; these DOM collections only recently became iterable. So if you're transpiling for older browsers (like IE11), that won't work. – T.J. Crowder Apr 27 '18 at 06:33
2

You cannot use forEach on HMTLCollection. forEach can only be used on `array.

Alternatives are, use lodash and do _.toArray(), which will convert the HTMLColelction to an array. After this, you can do normal array operations over it. Or, use ES6 spread and do forEach()

Like this,

var a = document.getElementsByTagName('div')
[...a].forEach()
Pranesh Ravi
  • 18,642
  • 9
  • 46
  • 70
0

form.elements, document.getElementsByTagName, document.getElementsByClassName and document.querySelectorAll return a node list.

A node list is essentially an object that doesn't have any methods like an array.

If you wish to use the node list as an array you can use Array.from(NodeList) or the oldschool way of Array.prototype.slice.call(NodeList)

// es6
const thingsNodeList = document.querySelectorAll('.thing')
const thingsArray = Array.from(thingsNodeList)
thingsArray.forEach(thing => console.log(thing.innerHTML))

// es5
var oldThingsNodeList = document.getElementsByClassName('thing')
var oldThingsArray = Array.prototype.slice.call(oldThingsNodeList)
thingsArray.forEach(function(thing){ 
  console.log(thing.innerHTML) 
})
<div class="thing">one</div>
<div class="thing">two</div>
<div class="thing">three</div>
<div class="thing">four</div>
synthet1c
  • 6,152
  • 2
  • 24
  • 39