39

I have a series of p tags on my page and I want to wrap them all into a container, e.g.

<p>foo</p>
<p>bar</p>
<p>baz</p>

I want to wrap all the above tags into a container as follows:

<div>
    <p>foo</p>
    <p>bar</p>
    <p>baz</p>
</div>

How to wrap a NodeList in an element using vanilla JavaScript?

6 Answers6

35

Posted below are a pure JavaScript version of jQuery's wrap and wrapAll methods. I can't guarantee they work exactly as they do in jQuery, but they do in fact work very similarly and should be able to accomplish the same tasks. They work with either a single HTMLElement or an array of them. I haven't tested to confirm, but they should both work in all modern browsers (and older ones to a certain extent).

Unlike the selected answer, these methods maintain the correct HTML structure by using insertBefore as well as appendChild.

wrap:

// Wrap an HTMLElement around each element in an HTMLElement array.
HTMLElement.prototype.wrap = function(elms) {
    // Convert `elms` to an array, if necessary.
    if (!elms.length) elms = [elms];

    // Loops backwards to prevent having to clone the wrapper on the
    // first element (see `child` below).
    for (var i = elms.length - 1; i >= 0; i--) {
        var child = (i > 0) ? this.cloneNode(true) : this;
        var el    = elms[i];

        // Cache the current parent and sibling.
        var parent  = el.parentNode;
        var sibling = el.nextSibling;

        // Wrap the element (is automatically removed from its current
        // parent).
        child.appendChild(el);

        // If the element had a sibling, insert the wrapper before
        // the sibling to maintain the HTML structure; otherwise, just
        // append it to the parent.
        if (sibling) {
            parent.insertBefore(child, sibling);
        } else {
            parent.appendChild(child);
        }
    }
};

See a working demo on jsFiddle.

wrapAll:

// Wrap an HTMLElement around another HTMLElement or an array of them.
HTMLElement.prototype.wrapAll = function(elms) {
    var el = elms.length ? elms[0] : elms;

    // Cache the current parent and sibling of the first element.
    var parent  = el.parentNode;
    var sibling = el.nextSibling;

    // Wrap the first element (is automatically removed from its
    // current parent).
    this.appendChild(el);

    // Wrap all other elements (if applicable). Each element is
    // automatically removed from its current parent and from the elms
    // array.
    while (elms.length) {
        this.appendChild(elms[0]);
    }

    // If the first element had a sibling, insert the wrapper before the
    // sibling to maintain the HTML structure; otherwise, just append it
    // to the parent.
    if (sibling) {
        parent.insertBefore(this, sibling);
    } else {
        parent.appendChild(this);
    }
};

See a working demo on jsFiddle.

Kevin Jurkowski
  • 1,244
  • 14
  • 15
  • 6
    Just as a heads up, you're better off not extending the prototype of HTMLElement as this could easily be unsafe and cause some major headaches (I believe it does if you're using jQuery). Use this as a global function instead. – Kevin Jurkowski Feb 22 '13 at 19:14
  • 2
    Great code! Just a though: I think the last if statement is redundant, because if sibling is null/undefined, parent.insertBefore(this, sibling) should be doing exactly the same as parent.appendChild(this). – Simon Steinberger May 09 '15 at 14:14
23

You can do like this:

// create the container div
var dv = document.createElement('div');
// get all divs
var divs = document.getElementsByTagName('div');
// get the body element
var body = document.getElementsByTagName('body')[0];

// apply class to container div
dv.setAttribute('class', 'container');

// find out all those divs having class C
for(var i = 0; i < divs.length; i++)
{
   if (divs[i].getAttribute('class') === 'C')
   {
      // put the divs having class C inside container div
      dv.appendChild(divs[i]);
   }
}

// finally append the container div to body
body.appendChild(dv);
Sarfraz
  • 377,238
  • 77
  • 533
  • 578
  • 4
    Good answer, but one nitpick/question - `document.body` is supported in every browser and is surely more efficient that searching the DOM for all instances of body and selecting by array index? – lucideer Jul 26 '10 at 18:36
  • @lucideer: That's true probably but i just went with convention :) – Sarfraz Jul 26 '10 at 18:42
  • 2
    @lucideer: “document.body... is surely more efficient” PREMATURE OPTIMISATION, I want to see screenshots of your benchmarks of `document.body` versus `document.getElementsByTagName('body')` in all modern browsers IMMEDIATELY :) – Paul D. Waite Jul 26 '10 at 18:57
  • 2
    @Paul - Ha. Touché, but I'm not really too into dogmatic assertions like laws on the prematurity of optimisation - if something is obviously more efficient without impairing readability, maintainability, portability, et al... yadayada. `document.body` is at the very least less verbose. And prettier. There's a lot to be said for pretty code. – lucideer Jul 26 '10 at 19:23
  • @sAc - It seems to me that this script only puts the 2nd and above div.c into div.container. Also, how does the script manage to move the div.c elements into div.container? I would have thought that appendChild would be creating new copies of the div.c elements. – KatieK Jul 26 '10 at 21:33
  • @KetieK: The `appendChild` can be used to append either new elements or existing ones. – Sarfraz Jul 26 '10 at 21:36
  • 3
    @KatieK Technically you should need to run `removeChild` first here, but running `appendChild` on an element that's already in DOM triggers an explicit `removeChild` call first (the spec. says all browsers should do this, and all do afaik). – lucideer Jul 30 '10 at 17:39
  • 3
    One issue with this script that I’ve just noticed (18 months later) is that it won’t wrap the existing `
    `s in-place: it’ll remove them from wherever they are in the document, put them into the container `
    `, then append the container `
    ` to the end of the document (which isn’t necessarily where the `
    `s were originally). That may be the desired behaviour, but I thought I’d point it out.
    – Paul D. Waite Feb 24 '12 at 10:30
10

I arrived at this wrapAll function by starting with Kevin's answer and fixing the problems presented below as well as those mentioned in the comments below his answer.

  1. His function attempts to append the wrapper to the next sibling of the first node in the passed nodeList. That will be problematic if that node is also in the nodeList. To see this in action, remove all the text and other elements from between the first and second <li> in his wrapAll demo.
  2. Contrary to the claim, his function won't work if multiple nodes are passed in an array rather than a nodeList because of the looping technique used.

These are fixed below:

// Wrap wrapper around nodes
// Just pass a collection of nodes, and a wrapper element
function wrapAll(nodes, wrapper) {
    // Cache the current parent and previous sibling of the first node.
    var parent = nodes[0].parentNode;
    var previousSibling = nodes[0].previousSibling;

    // Place each node in wrapper.
    //  - If nodes is an array, we must increment the index we grab from 
    //    after each loop.
    //  - If nodes is a NodeList, each node is automatically removed from 
    //    the NodeList when it is removed from its parent with appendChild.
    for (var i = 0; nodes.length - i; wrapper.firstChild === nodes[0] && i++) {
        wrapper.appendChild(nodes[i]);
    }

    // Place the wrapper just after the cached previousSibling,
    // or if that is null, just before the first child.
    var nextSibling = previousSibling ? previousSibling.nextSibling : parent.firstChild;
    parent.insertBefore(wrapper, nextSibling);

    return wrapper;
}

See the Demo and GitHub Gist.

Community
  • 1
  • 1
Web_Designer
  • 72,308
  • 93
  • 206
  • 262
  • 2
    Fantastic solution. For versatility, how would I change this to accept a single node instead of a node list? It trips up at `nodes[0].parentNode;`. – jkupczak Jun 21 '17 at 18:03
  • 1
    @jkupczak The first argument must be an [**indexed collection**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections) of nodes. You could simply put your node in an array: `wrapAll(nodes[0].parentNode, wrapper);` – Web_Designer Jun 22 '17 at 05:34
5

Here's my javascript version of wrap(). Shorter but you have to create the element before calling the function.

HTMLElement.prototype.wrap = function(wrapper){
  
  this.parentNode.insertBefore(wrapper, this);
  wrapper.appendChild(this);
}

function wrapDiv(){
  
  var wrapper = document.createElement('div'); // create the wrapper
  wrapper.style.background = "#0cf"; // add style if you want
  
  var element = document.getElementById('elementID'); // get element to wrap
  
  element.wrap(wrapper);
}
div {
  border: 2px solid #f00;
  margin-bottom: 10px;
}
<ul id="elementID">
  <li>Chair</li>
  <li>Sofa</li>
</ul>

<button onclick="wrapDiv()">Wrap the list</button>
pmrotule
  • 9,065
  • 4
  • 50
  • 58
3

If you're target browsers support it, the document.querySelectorAll uses CSS selectors:

var targets = document.querySelectorAll('.c'),
  head = document.querySelectorAll('body')[0],
  cont = document.createElement('div');
  cont.className = "container";
for (var x=0, y=targets.length; x<y; x++){
  con.appendChild(targets[x]);
}
head.appendChild(cont);
jerone
  • 16,206
  • 4
  • 39
  • 57
Rixius
  • 2,223
  • 3
  • 24
  • 33
3

Taking @Rixius 's answer a step further, you could turn it into a forEach loop with an arrow function

let parent = document.querySelector('div');
let children = parent.querySelectorAll('*');
let wrapper = document.createElement('section');

wrapper.className = "wrapper";

children.forEach((child) => {
    wrapper.appendChild(child);
});

parent.appendChild(wrapper);
* { margin: 0; padding: 0; box-sizing: border-box; font-family: roboto; }
body { padding: 5vw; }
span,i,b { display: block; }

div { border: 1px solid lime; margin: 1rem; }
section { border: 1px solid red; margin: 1rem; }
<div>
 <span>span</span>
 <i>italic</i>
 <b>bold</b>
</div>
Oneezy
  • 4,881
  • 7
  • 44
  • 73