16

I am attempting to merge two arrays of objects to so I can validate a form. The usual concat method does not appear to work in this circumstance. Concat works with ordinary numerical and string arrays but doesn't with object arrays. The line var allTags = allInputs.concat(allSelects); does not work.

var allInputs = document.getElementsByTagName("input");
alert("Inputs: " + allInputs.length);

var allSelects = document.getElementsByTagName("select");
alert("Selects: " + allSelects.length);

var allTags = allInputs.concat(allSelects);
alert("allTags: " + allTags.length);
Jordan Whitfield
  • 1,413
  • 15
  • 17
user2035797
  • 161
  • 1
  • 1
  • 3

5 Answers5

26

Concat works with ordinary numerical and string arrays but doesn't with object arrays.

Actually, it does, but NodeList instances don't have a concat method, and Array#concat doesn't have a means of identifying that you want to flatten those (because they're not arrays).

But it's still fairly easy to do (see caveat below, though). Change this line:

var allTags = allInputs.concat(allSelects);

to

var allTags = [];
allTags.push.apply(allTags, allInputs);
allTags.push.apply(allTags, allSelects);

Live Example | Source

That works by using a bit of a trick: Array#push accepts a variable number of elements to add to the array, and Function#apply calls the function using the given value for this (in our case, allTags) and any array-like object as the arguments to pass to it. Since NodeList instances are array-like, push happily pushes all of the elements of the list onto the array.

This behavior of Function#apply (not requiring the second argument to really be an array) is very clearly defined in the specification, and is well-supported in modern browsers.

Sadly, IE6 and 7 don't support the above (I think it's specifically using host objects — NodeLists — for Function#apply's second argument), but then, we shouldn't be supporting them, either. :-) IE8 doesn't, either, which is more problematic. IE9 is happy with it.

If you need to support IE8 and earlier, sadly, I think you're stuck with a boring old loop:

var allInputs = document.getElementsByTagName('input');
var allSelects = document.getElementsByTagName('select');
var allTags = [];

appendAll(allTags, allInputs);
appendAll(allTags, allSelects);

function appendAll(dest, src) {
  var n;

  for (n = 0; n < src.length; ++n) {
    dest.push(src[n]);
  }

  return dest;
}

Live Example | Source

That does work on IE8 and earlier (and others).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
2

document.get[something] returns a NodeList, which looks very similar to an Array, but has numerous distinctive features, two of which are:

  • It does not contain the concat method
  • The list of items is live. This means they change as the document changes in real time.

If you don't have any issues with turning your NodeLists into actual arrays, you could do the following to achieve the effect you desire:

var allInputs  = document.getElementsByTagName("input")
  , allSelects = document.getElementsByTagName("select")
;//nodeLists

var inputList  = makeArray(allInputs)
  , selectList = makeArray(allSelects)
;//nodeArrays

var combined   = inputList.concat(selectList);

function makeArray(list){
  return Array.prototype.slice.call(list);
}

You will lose any and all behaviors of the original NodeList, including it's ability to update in real time to reflect changes to the DOM, but my guess is, that's not really an issue.

THEtheChad
  • 2,372
  • 1
  • 16
  • 20
  • Be forewarned that, like the similar solution I give in the first part of my answer, this won't work in IE8 or earlier. (Try it here: http://jsbin.com/iledok/1) *sigh* At least IE9 is decent. So for IE8 and earlier, one needs a boring old loop. – T.J. Crowder Feb 04 '13 at 09:26
1

What you're dealing with are HTMLCollections, and they're live collections (i.e., when you add a node to the DOM, it's automatically added to the collection.

And sadly, it doesn't have a concat method... for a reason. What you need to do is to convert it to an array, losing its live behaviour:

var allInputsArray = [].slice.call(allInputs),
    allSelectsArray = [].slice.call(allSelects),
    allFields = allInputsArray.concat(allSelectsArray);
MaxArt
  • 22,200
  • 10
  • 82
  • 81
1

I found a simple way to collect dom objects in the order they appear on a page or form. Firstly, create a dummy class style 'reqd' on the object then use the following which creates a list in the order they appear on the page. Just add the class to any objects you wish to collect.

var allTags = document.querySelectorAll("input.reqd, select.reqd");
FutureMode
  • 11
  • 1
0

Looked into underscore.js at all? You could do

 var allSelectsAndInputs = _.union(_.values(document.getElementsByTagName("input")),_.values(document.getElementsByTagName("select")));

For more information see: http://underscorejs.org/#extend

The function is intended for objects, but works in this setting.

Zeke Nierenberg
  • 2,216
  • 1
  • 19
  • 30