32

There are similar questions, but all the answers are for swapping html elements only for the content inside.

I need to swap two divs, with lots of content in them (tables, select boxes, inputs, etc.). The elements have event listeners on them, so I need to preserve them after swapping the main divs.

I have access to jQuery 1.5. So answers with it are OK.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Ext
  • 657
  • 1
  • 8
  • 13

9 Answers9

72

To swap two divs without losing event handlers or breaking DOM references, you can just move them in the DOM. The key is NOT to change the innerHTML because that recreates new DOM nodes from scratch and all prior event handlers on those DOM objects are lost.

But, if you just move the DOM elements to a new place in the DOM, all events stay attached because the DOM elements are only reparented without changing the DOM elements themselves.

Here's a quick function that would swap two elements in the DOM. It should work with any two elements as long as one is not a child of the other:

function swapElements(obj1, obj2) {
    // create marker element and insert it where obj1 is
    var temp = document.createElement("div");
    obj1.parentNode.insertBefore(temp, obj1);

    // move obj1 to right before obj2
    obj2.parentNode.insertBefore(obj1, obj2);

    // move obj2 to right before where obj1 used to be
    temp.parentNode.insertBefore(obj2, temp);

    // remove temporary marker node
    temp.parentNode.removeChild(temp);
}

You can see it work here: http://jsfiddle.net/jfriend00/NThjN/


And here's a version that works without the temporary element inserted:

function swapElements(obj1, obj2) {
    // save the location of obj2
    var parent2 = obj2.parentNode;
    var next2 = obj2.nextSibling;
    // special case for obj1 is the next sibling of obj2
    if (next2 === obj1) {
        // just put obj1 before obj2
        parent2.insertBefore(obj1, obj2);
    } else {
        // insert obj2 right before obj1
        obj1.parentNode.insertBefore(obj2, obj1);

        // now insert obj1 where obj2 was
        if (next2) {
            // if there was an element after obj2, then insert obj1 right before that
            parent2.insertBefore(obj1, next2);
        } else {
            // otherwise, just append as last child
            parent2.appendChild(obj1);
        }
    }
}

Working demo: http://jsfiddle.net/jfriend00/oq92jqrb/

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I might add, that this code is the only one that actually works, except for 2 corner cases: swapping the n-first or n-last item. – philk Nov 10 '15 at 01:26
  • 1
    @philk - I don't understand what corner cases you think don't work. Please explain. Perhaps show a jsFiddle. – jfriend00 Nov 10 '15 at 01:32
  • Really nice work here! Performance-wise, which do you suggest doing? – Jessica Jun 15 '16 at 21:43
  • 1
    @Jessica - You'd have to run something like a jsPerf on both to test out performance, but I would assume the second one (that doesn't create a temporary element) would more likely be faster. – jfriend00 Jun 15 '16 at 21:47
  • @jfriend00 Thanks! Also, I noticed you used `nextSibling` for the declaration of `next2`. If you do `nextSibling`, as opposed to `nextElementSibling`, then the function will **always** execute the second possible `if` option only. But if you do nextElementSibling, then all the if statements are needed. (I'm therefore assuming the following way is better) http://jsfiddle.net/8uyptyv0/ – Jessica Jun 15 '16 at 23:07
  • @Jessica - The way I have my code is more generic and will work with any type of nodes, even text nodes. Also, it's compatible with older versions of IE that don't support `nextElementSibling`. The version of your code currently at http://jsfiddle.net/8uyptyv0/ is not using `nextElementSibling` and appears to have removed code that is needed in some cases. – jfriend00 Jun 16 '16 at 06:47
  • I don't think you need to check if `next2`, since insertBefore appends if the ref element is null anyway – Brian Leishman Jun 16 '21 at 15:10
8

This is a simpler function for swapping two elements without actually reloading the element...

function swapElements(obj1, obj2) {
    obj2.nextSibling === obj1
    ? obj1.parentNode.insertBefore(obj2, obj1.nextSibling)
    : obj1.parentNode.insertBefore(obj2, obj1); 
}

Note: if the obj1 has an embedded video like YouTube, it will not reload when swapped. It's just the position of the elements that changed.

Robert
  • 186
  • 5
  • 13
  • 2
    very elegant and without the marker div answer that was most voted here – philk Jun 09 '14 at 23:26
  • 2
    I believe in this answer, `obj2` has to be after `obj1`. If `obj1` were before `obj2`, they would not swap. @jfriend00 answer will swap both elements regardless if they are before or after each other. – aug Oct 02 '14 at 01:08
  • 3
    This only works properly if elements are right next to each other with the same parent. Separate them by a few elements or give them different parents and it does not swap them. It just moves one before the other. Just try [this jsFiddle](http://jsfiddle.net/jfriend00/pp3op71k/) and press the "Swap A C" button. – jfriend00 Nov 10 '15 at 02:42
  • Very elegant. Works perfect when sorting lists –  Aug 21 '19 at 17:17
1

If you keep track of the two elements' parentNodes and the nextSibling of one, you can swap two elements with their children without any temporary placeholders.

If the second element is an only child, or the last child of its parent, its replacement is (properly) appended to the parent.

function swap(a, b){
    var p1= a.parentNode, p2= b.parentNode, sib= b.nextSibling;
    if(sib=== a) sib= sib.nextSibling;
    p1.replaceChild(b, a);
    if(sib) p2.insertBefore(a, sib);
    else p2.appendChild(a);
    return true;
}
naugtur
  • 16,827
  • 5
  • 70
  • 113
kennebec
  • 102,654
  • 32
  • 106
  • 127
1

My simple function seems to be the shortest and most widely compatible. It doesn't need placeholders or reload elements or anything messy like that:

function swapElements(el1, el2) {
    var p2 = el2.parentNode, n2 = el2.nextSibling
    if (n2 === el1) return p2.insertBefore(el1, el2)
    el1.parentNode.insertBefore(el2, el1);
    p2.insertBefore(el1, n2);
}
cronoklee
  • 6,482
  • 9
  • 52
  • 80
0

This somewhat depends on how your events are bound to the elements you're swapping. If you're using jQuery, then this answer is all you need, really.

The main thing is not to delete or removeChildnode the elements, and just copy-paste the html elsewhere. In doing so, some browsers will clear all resources (event handlers included) so, bound events will be lost. Though IE is notorious for leaking memory when it comes to dynamically bound events. It's hard to predict how various browsers will behave, really.

Basically, I'd say: swap elements in any way you like, except for copy/paste HTML strings, and bind your events using jQuery's .live() method. Since I don't think 1.5 has the .on method

Community
  • 1
  • 1
Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
  • Copy, pasting the HTML somewhere else will create brand new nodes that do NOT have the same event handlers as the original nodes. So, this will not work. – jfriend00 May 23 '12 at 09:53
  • I know, that's why I said `swap elements in any way you like, except for copy/paste HTML strings`. I stressed this twice, even. copy-pasting HTML is the devils work, IMO. Come to think about it, in this case I'd suggest cloning the nodes... – Elias Van Ootegem May 23 '12 at 11:08
0

Since jQuery is tagged in the question, here is a jQuery solution:

  $('#el_to_move').appendTo('#target_parent_el');

That's it. jQuery will cut/paste it to the new location.


This could also be helpful:

https://api.jquery.com/detach/

cssyphus
  • 37,875
  • 18
  • 96
  • 111
0

This all is very strange considering the fact that o1.swapNode(o2); works very well in IE for very long time. In my case of object and text nodes intermixed, works only one version (already shown here):

let t = document.createElement("div");
o1.parentNode.insertBefore(t, o1);
o2.parentNode.insertBefore(o1, o2);
t.parentNode.insertBefore(o2, t);
t.parentNode.removeChild(t);

Yes, I hate creating temporary node, but the above version without this trick fails if you use it straight (without calling function). Sometimes you don't want to call a function.

0

It's really easy if you're swapping elements next to each other:

<div class="container">
   <div class="item">Item 1</div>
   <div class="item">Item 2</div>
</div>

$('.container .item:nth-child(2)').insertBefore('.container .item:nth-child(1)');

0

Internet Explorer is dead

All modern browsers have:

function swapDOMElements( el1 , el2, temp=el2.previousElementSibling){
  el1.after(e2);
  temp.after(el1);
}
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49