A simple way to sort HTML lists / localeCompare()

A simple way to sort HTML lists / localeCompare()

11 Apr 2020 string-comparison sorting javascript

Something you see asked all the time is how to sort an HTML list. There's no built-in support for this, of course, and over the years there's been implementations and plugins, notably a plethora of jQuery plugins when jQuery was big.

Here's how I do it, with just a few lines of code!

let list = document.querySelector('ul#mylist'); let orderedLIs = [...list.children].sort((a, b) => a.textContent.localeCompare(b.textContent) ); list.innerHTML = orderedLIs.map(li => li.outerHTML).join('');

So what's actually happening? Well, first we grab our list element. Then we derive an array from its child element nodes. We do this by feeding children, which is an HTML collection, to a new array via spread syntax. This effectively converts it from an object to an array, and means we can use Array.sort() on the items in it - specifically, sorting based on the text content of each child node.

We do that via String.localeCompare(), which is something I want to highlight in this article as it doesn't get nearly enough love, even though it's been around since JavaScript 1.2. It makes sorting absolutely trivial. Before we get to that, I should round off the explainer by saying we then map the newly-sorted collection into a new array of HTML (each item in the array containing the HTML of the li tag) and appending the whole thing back to our list item.

An ode to String.localeCompare()

Computational sorting is a fascinating subject and most of it is off-topic for this article. Suffice is it to say, though, that the ECMAScript specification doesn't actually specify which sorting algorithm (and there are lots) a browser should use, nor whether the algorithm should be stable.

So it is that different browsers use different algorithms. For a good period, Chrome's V8 engine even used two different algorithms depending on how many elements you had in your array.

JavaScript shields you from the horrors of writing sort algorithms by providing Array.sort(). Your job is only to pass it a callback function, accepting two params, which will be populated by items from the array that are then compared in your callback. The typical implementation looks like this:

let foo = ['z', 'a', 'f']; foo.sort((a, b) => a > b ? 1 : -1); //["a", "f", "z"]

The problem is, as non-English-speaking developers everywhere know and as this blog post illustrates, this fails for pretty much all languages except English. As the author says:

According to the rules, the German Umlauts (ä, ö, ü) should come right after their respective vowels: a, ä, b, o, ö. But here's where we're facing a problem, because "ä" > "b". You see, the unicode of b is 0x62 and ä is 0xE4 and 0x62 < 0xE4.

And so Array.sort(), with the standard implementation shown above, will put these German letters in the wrong order:

['ä', 'b', 'a'].sort((a, b) => a > b ? 1 : -1); //gives ["a", "b", "ä"] - but ä should be after a

This is where String.localeCompare() comes in. String.localeCompare() uses locale-based character ordering rather than Unicode ordering. It compares a subject string against a comparator string, and returns a negative, positive or zero number depending on whether the subject is "before", "after" or identical to the comparator, like so:

'b'.localeCompare('c'); //-1 - b is before c 'b'.localeCompare('a'); //1 - b is after a 'b'.localeCompare('b'); //0 - b === b

It's crucial to understand that, by default, String.localeCompare() sorts according to the user's OS-level locale. So different users in different countries may see different sort orders when they access your script - unless, that is, you enforce a language via the second argument:

"a".localeCompare('b', 'en'); //force English alphabetical comparison

Don't rely on the method returning exactly -1 or 1! The spec requires only that the method return a negative, positive or zero number, so a browser implementation could, in theory, return -2 rather than -1.

Let's try the German example again with this method:

['ä', 'b', 'a'].sort((a, b) => a.localeCompare(b)); //gives ["a", "ä", "b"]

Success! So there you have it. A brief demo of sorting list elements turned into a segue about the joys of String.localeCompare().

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!