88

If I have a list like this:

<ul id="mylist">
    <li id="list-item1">text 1</li>
    <li id="list-item2">text 2</li>
    <li id="list-item3">text 3</li>
    <li id="list-item4">text 4</li>
</ul>

What's the easiest way to re-arrange the DOM nodes to my preference? (This needs to happen automatically when the page loads, the list-order preference is gained from a cookie)

E.g.

<ul id="mylist">
    <li id="list-item3">text 3</li>
    <li id="list-item4">text 4</li>
    <li id="list-item2">text 2</li>
    <li id="list-item1">text 1</li>
</ul>
Dori
  • 915
  • 1
  • 12
  • 20
James
  • 109,676
  • 31
  • 162
  • 175
  • I'm not sure if this will help you, but if you're using a server side technology (like PHP) on this page, then it should also have access to the cookies. in PHP, it's in the global $_COOKIE array. – nickf Nov 12 '08 at 01:05

11 Answers11

94

Though there's probably an easier way to do this using a JS Library, here's a working solution using vanilla js.

var list = document.getElementById('mylist');

var items = list.childNodes;
var itemsArr = [];
for (var i in items) {
    if (items[i].nodeType == 1) { // get rid of the whitespace text nodes
        itemsArr.push(items[i]);
    }
}

itemsArr.sort(function(a, b) {
  return a.innerHTML == b.innerHTML
          ? 0
          : (a.innerHTML > b.innerHTML ? 1 : -1);
});

for (i = 0; i < itemsArr.length; ++i) {
  list.appendChild(itemsArr[i]);
}
nickf
  • 537,072
  • 198
  • 649
  • 721
  • Thanks for your solution NickF, I'm gonna adapt it a bit since I'm using jQuery but I think I have an idea of how to do it now, thanks! :) – James Nov 12 '08 at 01:56
  • 47
    By using `list.children` instead of `list.childNodes`, you can avoid the check for text nodes. – Livingston Samuel Apr 27 '10 at 15:26
  • When I first read this answer, I thought vanilla js? Why is [vanilla.js](http://vanilla-js.com/) required here? :^) – zipzit Nov 16 '17 at 17:59
  • 4
    @LivingstonSamuel ...but on the expense of not working in IE (even IE11!) – leo Sep 26 '18 at 13:52
  • 1
    https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children#Browser_compatibility – minj Nov 07 '19 at 18:27
  • Looks like it's fully supported now. – spekulatius Nov 05 '21 at 18:20
  • 1
    The `sort` callback can be simplified to `(a, b) => a.innerHTML.localeCompare(b.innerHTML)` or `({ innerHTML: a }, { innerHTML: b }) => a.localeCompare(b)`. There are a few other more modern ways of writing a solution to this problem and other optimizations. Here’s a modern example: [sort div's through sort();](/a/75152213/4642212). – Sebastian Simon Jan 17 '23 at 21:07
89

Use ES6 syntax to re-sort children:

var list = document.querySelector('#test-list');

[...list.children]
  .sort((a,b)=>a.innerText>b.innerText?1:-1)
  .forEach(node=>list.appendChild(node));
ashleedawg
  • 20,365
  • 9
  • 72
  • 105
ahuigo
  • 2,929
  • 2
  • 25
  • 45
24

You might find that sorting the DOM nodes doesn't perform well. A different approach would be to have in your javascript an array that represents the data that would go into the DOM nodes, sort that data, and then regenerate the div that holds the DOM nodes.

Maybe you dont' have that many nodes to sort, so it wouldn't matter. My experience is based on trying to sort HTML tables by manipulating the DOM, including tables with hundreds of rows and a couple dozen columns.

Corey Trager
  • 22,649
  • 18
  • 83
  • 121
13

See it in action: http://jsfiddle.net/stefek99/y7JyT/

    jQuery.fn.sortDomElements = (function() {
        return function(comparator) {
            return Array.prototype.sort.call(this, comparator).each(function(i) {
                  this.parentNode.appendChild(this);
            });
        };
    })();

Terse

Mars Robertson
  • 12,673
  • 11
  • 68
  • 89
  • Nice! I also like things terse, so based on that, I came up with a [very simple jQuery function](http://jsfiddle.net/mindplay/H2mrp/) that lets you provide a callback-function that computes the value to sort by. (I included two examples: sorting a list alphabetically, and sorting a table by a numeric value in the first column - both essentially one-liners.) – mindplay.dk Oct 03 '13 at 21:30
  • 1
    I know this is an old post, but may i ask, what is the benefit of using a self executing function return the sortDomElements function? Wouldn't it be more terse to say jQuery.fn.sortDomElements = function(comparitor)... ? like this http://jsfiddle.net/y7JyT/77/ – Julian Aug 02 '16 at 23:56
11

My version, hope will be useful for others:

var p = document.getElementById('mylist');
Array.prototype.slice.call(p.children)
  .map(function (x) { return p.removeChild(x); })
  .sort(function (x, y) { return /* your sort logic, compare x and y here */; })
  .forEach(function (x) { p.appendChild(x); });
Ebrahim Byagowi
  • 10,338
  • 4
  • 70
  • 81
  • What do you mean `/* sort logic */`? :/ The code looks good, but my sort logic is a bit weak – Arthur Tarasov Jul 16 '17 at 10:42
  • 1
    It means you should put your sort logic there, have a look at https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/sort it basically the same, only x and y are DOM nodes. – Ebrahim Byagowi Jul 16 '17 at 11:06
8

If you're already using jQuery, I'd recommend tinysort : http://tinysort.sjeiti.com/

$("li").tsort({order:"asc"});
$("li").tsort({order:"desc"});
Jay
  • 309
  • 3
  • 13
  • 2
    Tinysort is multiple times slower than 13 lines of code, based on sorting jquery selection result and appending it back - http://jsperf.com/attr-vs-getattribute/12 – mas.morozov Oct 04 '14 at 22:26
  • TinySort used to be a jQuery plugin but was rewritten to remove the jQuery dependency. It is now smaller and faster – Giacomo Pigani Apr 24 '17 at 18:22
7

Here's an ES6 function to sort DOM nodes in place:

const sortChildren = ({ container, childSelector, getScore }) => {
  const items = [...container.querySelectorAll(childSelector)];

  items
    .sort((a, b) => getScore(b) - getScore(a))
    .forEach(item => container.appendChild(item));
};

Here's how you would use it to sort Untapped user reviews by score:

sortChildren({
  container: document.querySelector("#main-stream"),
  childSelector: ".item",
  getScore: item => {
    const rating = item.querySelector(".rating");
    if (!rating) return 0;
    const scoreString = [...rating.classList].find(c => /r\d+/.test(c));
    const score = parseInt(scoreString.slice(1));
    return score;
  }
});
cgenco
  • 3,370
  • 2
  • 31
  • 36
1

without analyzing too much if this brings anything new to the table, i usually use this:

function forEach(ar, func){ if(ar){for(var i=ar.length; i--; ){ func(ar[i], i); }} }
function removeElement(node){ return node.parentNode.removeChild(node); }
function insertBefore(ref){ return function(node){ return ref.parentNode.insertBefore(node, ref); }; }

function sort(items, greater){ 
    var marker = insertBefore(items[0])(document.createElement("div")); //in case there is stuff before/after the sortees
    forEach(items, removeElement);
    items.sort(greater); 
    items.reverse(); //because the last will be first when reappending
    forEach(items, insertBefore(marker));
    removeElement(marker);
} 

where item is an array of children of the same parent. we remove starting with the last and append starting with the first to avoid flickering in the top part which is probably on screen. i usually get my items array like this:

forEachSnapshot(document.evaluate(..., 6, null), function(n, i){ items[i] = n; });
peter
  • 429
  • 2
  • 7
  • 16
0

The neatest way I can think of:

The param compare is just like the compare function used in Array.sort().

Sort child nodes.

/**
 * @param {!Node} parent
 * @param {function(!Node, !Node):number} compare
 */
function sortChildNodes(parent, compare) {
  const moveNode = (newParent, node) => {
    // If node is already under a parent, append() removes it from the
    // original parent before appending it to the new parent.
    newParent.append(node);
    return newParent;
  };
  parent.append(Array.from(parent.childNodes) // Shallow copies of nodes.
                    .sort(compare) // Sort the shallow copies.
                    .reduce(moveNode, document.createDocumentFragment()));
}

Sort child elements (a subset of child nodes).

/**
 * @param {!Element} parent
 * @param {function(!Element, !Element):number} compare
 */
function sortChildren(parent, compare) {
  const moveElement = (newParent, element) => {
    // If element is already under a parent, append() removes it from the
    // original parent before appending it to the new parent.
    newParent.append(element);
    return newParent;
  };
  parent.append(Array.from(parent.children) // Shallow copies of elements.
                    .sort(compare) // Sort the shallow copies.
                    .reduce(moveElement, document.createDocumentFragment()));
}
Leedehai
  • 3,660
  • 3
  • 21
  • 44
0

In case you need numerical sorting, here is the short solution that worked for me:
(if you need alphabetical sorting, just remove the two + in the function, I think it's still a shorter solution than the currently most upvoted ones)

function sortSelectOpts(selectNode) {
    const optionNodes = Array.from(selectNode.children);
    optionNodes.sort((a,b) => +a.innerText > +b.innerText ?1:-1);
    optionNodes.forEach((option) => selectNode.appendChild(option));
}

Which is to be used like this:

sortSelectOpts(document.getElementById('CircuitNumber'));
stallingOne
  • 3,633
  • 3
  • 41
  • 63
0

You can avoid using appendChild in a loop with a single call of replaceChildren. Also localeCompare might be convenient since it always returns 0, 1, or -1.

const list = document.getElementById("mylist");
const children = Array.from(list.children).sort((a,b) => a.textContent.localeCompare(b.textContent));
list.replaceChildren(...children);
JukkaP
  • 1,994
  • 1
  • 14
  • 7