3

I have 5 div's created in HTML, and I want to add all of them into a div wrapper I created in JavaScript. I tried looping through the 5 div's via a for-in loop, then append the div as a child of the wrapper.

For some reason, the for loop changes the 5 div's order and doesn't append all of them in wrapper. How can I add all div's to wrapper, keeping the (HTML) order, using JavaScript?

(Please don't post JQuery answers because that isn't the question. I want JavaScript answers only.)

JSFiddle

var wrapper = document.createElement('div'),
  myClass = document.getElementsByClassName('myClass');

myClass[0].parentElement.appendChild(wrapper);
wrapper.id = 'wrapper';

for (var key in myClass) {
  if (!myClass.hasOwnProperty(key)) continue;

  wrapper.appendChild(myClass[key]);
}
#wrapper {
  border: 2px solid green;
  color: brown;
}
<div class="myClass">First</div>
<div class="myClass">Second</div>
<div class="myClass">Third</div>
<div class="myClass">Fourth</div>
<div class="myClass">Fifth</div>
Jessica
  • 7,791
  • 10
  • 44
  • 89
  • 2
    [If you want order, don't iterate using `for...in`!!!](http://stackoverflow.com/q/500504/1529630). There is also the problem that the collection is live. – Oriol Feb 07 '16 at 23:59
  • 1
    Also, HTMLCollections have additional enumerable properties other than the indexes for their members (e.g. *item*, *namedItem* and *length* and the IDs of elements that have them). – RobG Feb 08 '16 at 00:23
  • @RobG Luckily these are inherited properties, so the `hasOwnProperty` check avoids this additional problem. – Oriol Feb 08 '16 at 00:29
  • @Oriol— *length* and element IDs aren't inherited, though enumerability differs between browsers. – RobG Feb 08 '16 at 02:18

4 Answers4

6

The document.getElementsByClassName method returns an HTMLCollection-object, which is similar to an array, as in it has numeric keys which should be used.

e.g. for (var i = 0; i < myClass.length; ++i)

Once you use an incremental numeric index, you'll notice it actually behaves the same as your key in myClass, which is rather logical, as the key is the numeric index.

What is happening is that an HTMLCollection represents elements in document order (a so called live list, which reflects the changes in the DOM) and you are moving them around by appending them to the wrapper element (hence the order within the HTMLCollection changes too).

There are several tricks to work around this, the one closest to your current setup would be to walk through the HTMLCollection from end to start and insertBefore instead of appendChild

for (var len = myClass.length - 1; len >=0; --len) {
    wrapper.insertBefore(myClass[len], wrapper.firstChild);
}

insertBefore fiddle

This works because the wrapper is (in your example) after the elements you're moving into it, therefor not changing the order of the elements.

There is another (easier) approach: document.querySelectorAll. The querySelectorAll method returns a (static) NodeList, so you can safely assume the order will not change while you move nodes around.

The syntax is (IMHO) more convenient than getElementsByClassname, as it uses CSS Selectors (much like the popular javascript framework we won't mention)

querySelectorAll fiddle

Rogier Spieker
  • 3,657
  • 2
  • 15
  • 21
  • An alternative is to add the elements to *wrapper* before adding it to the DOM so as they're moved, they are removed from the DOM and hence the collection also. – RobG Feb 08 '16 at 00:19
  • @Rogier Spieker Lol! You had me laughing by your last line! I didn't mean it like that, I just didn't want JQuery answers when there isn't a JQuery tag. But I like your answer! Thank you! I think I'm going to go with the `document.querySelectorAll` approach. – Jessica Feb 08 '16 at 00:26
  • @RobG I don't quite understand what I gain by doing that. – Jessica Feb 08 '16 at 00:34
  • What @RobG is saying is that by not having the wrapper element attached to the DOM, the collection returned by `getElementsByClassName` will be emptied, e.g. `while (myClass.length > 0) {wrapper.appendChild(myClass[0]);}` (this is what [Maciej Kwas' answer does](http://stackoverflow.com/a/35260750/2579117)). The `querySelectorAll`-approach is more robust, as it will work more consistently over different conditions (the wrapper can be anywhere in-/outside the DOM) – Rogier Spieker Feb 08 '16 at 00:53
  • @RogierSpieker Oh I get it. Do you recommend adding the elements to wrapper before or after adding it to the DOM? – Jessica Feb 08 '16 at 02:54
  • 1
    @Jessica I'd attach the wrapper to the DOM as late as possible. I'd always try to keep the number of DOM-manipulations to a minimum, as these will trigger a [reflow](http://stackoverflow.com/questions/27637184/what-is-dom-reflow) in the browser. The least amount of changes in the DOM would be to add the elements to the wrapper and then add the wrapper to the document, so that would be my choice for this scenario. – Rogier Spieker Feb 08 '16 at 08:44
  • Use a DocumentFragment rather – caub Jul 24 '16 at 12:17
3

Look at your for loop step by step:

1. myClass[First, Second, Third, Fourth, Fifth]; wrapper[] ; key = 0
2. myClass[Second, Third, Fourth, Fifth] ; wrapper[First]; key = 1

Now instead of Second, you'll take Third, because key is 1, but you'll need to take the item at index 0. This also gives the fix: always take the item at position 0.

var wrapper = document.createElement('div'),
  myClass = document.getElementsByClassName('myClass');

myClass[0].parentElement.appendChild(wrapper);
wrapper.id = 'wrapper';

for (var i = 0; i < myClass.length; i++) {
  wrapper.appendChild(myClass[0]);
}
#wrapper {
  border: 2px solid green;
  color: brown;
}
<div class="myClass">First</div>
<div class="myClass">Second</div>
<div class="myClass">Third</div>
<div class="myClass">Fourth</div>
<div class="myClass">Fifth</div>
Gavriel
  • 18,088
  • 12
  • 63
  • 98
1

You are changing the collection on the fly (in the loop itself) by removing items from it, that's why it acts wired. Here's the code that should actually work:

var wrapper = document.createElement('div'),
myClass = document.getElementsByClassName('myClass'),
myClassParent = myClass[0].parentNode;

while (myClass.length) {
    wrapper.appendChild(myClass[0]);
}
myClassParent.appendChild(wrapper);
wrapper.setAttribute('id','wrapper');

https://jsfiddle.net/byd9fer3/1/

Maciej Kwas
  • 5,417
  • 2
  • 24
  • 43
  • You can construct `for` loop as well for that, it's just easier condition to check. – Maciej Kwas Feb 08 '16 at 16:55
  • I tried it with a for loop and it didn't work as expected. Here's an updated JSFiddle. – Jessica Feb 08 '16 at 17:49
  • @Jessica read Gavriel's answer and then Oddadmix answer and comments, then you'll understand why in that matter `for` loop iterating from lowest index to highest doesn't work, but reversed would. – Maciej Kwas Feb 08 '16 at 18:38
  • Everything is in other's answers https://jsfiddle.net/byd9fer3/3/ , all you need now is a bit of logical sense. – Maciej Kwas Feb 08 '16 at 19:02
0

I have a simple working version for you with code. Thanks

<html>
<body>

<button onclick="myFunction()">Try it</button>

<p><strong>Note:</strong> The getElementsByClassName() method is not supported in Internet Explorer 8 and earlier versions.</p>

<div class="myClass">First</div>
<div class="myClass">Second</div>
<div class="myClass">Third</div>
<div class="myClass">Fourth</div>
<div class="myClass">Fifth</div>

<script>
function myFunction() {

    var wrapper = document.createElement('div');
    var x = document.getElementsByClassName("myClass");
    

for(var i=0; i < x.length;++i){
    var newdiv = document.createElement('div');
    newdiv.appendChild(document.createTextNode(x[i].innerHTML));
    wrapper.appendChild(newdiv);
   }

 document.body.appendChild(wrapper);
}
</script>

</body>
</html>
  • I don't think setting the `innerHTML` as the `textContent` is a good idea. Consider using `cloneNode`. – Oriol Feb 08 '16 at 00:26