6

Imagine some DOM elements:

<ul id="list">
  <li data-index="3">Baz</li>
  <li data-index="1">Foo</li>
  <li data-index="2">Bar</li>
</ul>

How can these elements be sorted using JavaScript and without jQuery?

Something similar to:

document.getElementById('list').sort(function(li) { return li.dataset.index; });
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • Try searching for this question online and you'll find nothing out there that covers this other than some jQuery stuff. Seems like a perfectly reasonable question to me. Explain why if you feel otherwise. – Drew Noakes Jun 21 '14 at 13:58
  • 1
    Put references to `li` elements into an array (or convert NodeList into one), sort, and insert elements back into DOM in array order … done. – CBroe Jun 21 '14 at 14:00
  • @CBroe, that involves a lot of DOM churn, which is bad for performance especially if the list is already mostly sorted. I'd like to do the sort in-place ideally. – Drew Noakes Jun 21 '14 at 14:01
  • What do you mean by in-place exactly here? Nothing about your shown pseudo-code say “in-place” to me … – CBroe Jun 21 '14 at 14:04
  • 1
    "In-place" sorting would probably be even slower, you'd need to append the same element several times. Btw. I didn't vote down, but maybe the lack of a true attempt to solve this has caused a down-vote. – Teemu Jun 21 '14 at 14:04
  • 2
    http://jsfiddle.net/f4s7s/1/ – CBroe Jun 21 '14 at 14:12
  • @Teemu, I don't see how this would involve appending the same element more than once. Also, for a simple topic I generally prefer short concise questions. When I search for answers on SO, I find other people's incorrect workings a distraction between the problem and its solution. Each language's community on SO has a different vibe, and the JS one is quick to assume the OP is lazy :) – Drew Noakes Jun 21 '14 at 14:35
  • @DrewNoakes Maybe I understood "in-place" somehow differently. I don't know about other "language communities", but here under JS tag we've [these](http://stackoverflow.com/questions/24341303/export-an-html-table-to-csv) kind of questions far too much : ). – Teemu Jun 21 '14 at 15:35
  • @Teemu, understood. By in-place I mean that no new array is created, and the elements are manipulated directly (in the parent's `children` collection I suppose). This means that if the items are already sorted, then no actual DOM manipulation occurs. In my case, I'm appending one child to a parent, then re-sorting. So most of the time I just need to insert that one child at the correct index to solve this. However I found no treatment of this topic via Google, so thought it might make an interesting SO question. – Drew Noakes Jun 21 '14 at 15:43
  • Well if you just need to insert one new element at a time into an already sorted listed of elements – then you could just loop through the existing elements, until you find the first one with a “larger” sort value, and insert the new one before that … – CBroe Jun 21 '14 at 18:01
  • @CBroe, yeah but it's not always a single element. In other cases the user may change the sort criteria. – Drew Noakes Jun 21 '14 at 18:39
  • @DrewNoakes The problem with the approach you've described in your comment is, that though HTMLCollections and NodeLists returned by many DOM methods are live, they are not bi-directional. I.e. a change made to a list is not reflected to the DOM. – Teemu Jun 21 '14 at 21:16
  • @Teemu, interesting. Still can't one just use `insertBefore` on the parent? – Drew Noakes Jun 21 '14 at 22:32
  • @DrewNoakes, of course you can use `insertBefore`. In the same way as `appendChild`, if an element is in the DOM already, it gets removed from its current position and is inserted at the new one. – CBroe Jun 22 '14 at 11:55

3 Answers3

5

You should use the ordering capabilities of flexboxes. This will allow to re-order the elements without moving them around in the DOM. This involves setting the CSS order property.

See https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Flexible_boxes for more details.

  • 1
    Good idea, but sadly [`attr()`](https://developer.mozilla.org/en-US/docs/Web/CSS/attr) currently only works for `content` property and strings. In the future, `order: attr(data-index integer)` will be great. – Oriol Jun 21 '14 at 15:56
  • Nice! Ordering should be a presentation concern. Support seems ok for the browsers I'm targetting (private, controlled deployment) but may not be suitable for everyone just yet: http://caniuse.com/#search=flexbox – Drew Noakes Jun 21 '14 at 15:56
  • 1
    @Oriol Don't understand your concern. In the initial code, you can just use `style='order: 1; '` instead of `data-index='1'`. Then, in the sorting code, you can set `elt.style.order` based on the result. –  Jun 21 '14 at 22:30
  • @torazaburo The problem is that I find inline styles ugly :) – Oriol Jun 22 '14 at 01:11
  • @Oriol (with a CSS preprocessor) you could easily create a handful of utility classes, which you'd then apply. – kano Sep 05 '16 at 14:50
3

Some time ago I wrote this:

function sortChildren(wrap, f, isNum) {
    var l = wrap.children.length,
        arr = new Array(l);
    for(var i=0; i<l; ++i)
        arr[i] = [f(wrap.children[i]), wrap.children[i]];
    arr.sort(isNum
        ? function(a,b){ return a[0]-b[0]; }
        : function(a,b){ return a[0]<b[0] ? -1 : a[0]>b[0] ? 1 : 0; }
    );
    var par = wrap.parentNode,
        ref = wrap.nextSibling;
    par.removeChild(wrap);
    for(var i=0; i<l; ++i) wrap.appendChild(arr[i][1]);
    par.insertBefore(wrap, ref);
}

Basically:

  1. First create an array to store the elements with its corresponding value returned by the comparator function.

    We could also run the function when sorting, but since DOM interactions are slow, this way we make sure the function will only run once per element.

  2. Then, we sort it using native sort.

    If isNum argument is truly, we use a sorting function that compares numerically. This is needed if the comparator function returns strings but you want to compare numerically instead of lexicographically.

  3. Now, we remove the wrapper element from the DOM. This way reordering the children will be less expensive (e.g. avoiding repaitings).

  4. Finally we reorder the children, and insert wrapper back in its place.

Run it like

sortChildren(
    document.getElementById('list'),
    function(li) { return li.dataset.index; },
    true
);

or

sortChildren(
    document.getElementById('list'),
    function(li) { return +li.dataset.index; }
);

Demo

Oriol
  • 274,082
  • 63
  • 437
  • 513
1
<!doctype html>
<html lang="en">
<head>
<meta charset= "utf-8">
<title>sort list</title>
</head>
<body>
<strong>Double click the list to sort by data-index</strong>
<ul id= "list">
<li data-index= "3">Baz</li>
<li data-index= "1">Foo</li>
<li data-index= "2">Bar</li>
</ul>
<script>

document.body.ondblclick=function(){
    var A=[], pa= document.getElementById('list');
    var itemz= pa.getElementsByTagName('li');

    A.slice.call(itemz).sort(function(a, b){
        return a.getAttribute('data-index')-b.getAttribute('data-index');
    }).forEach(function(next){
            pa.appendChild(next);
    });
}

</script>
</body>
</html>
kennebec
  • 102,654
  • 32
  • 106
  • 127