JavaScript Sort DOM

Viewed 62

I am trying to sort DOM based on a data-* attribute. The DOM looks like this, albeit repeated and with incrementing ids and different image source:

<div class="col mb-3" data-id="1" data-filter="Owned">
  <div class="card template">
    <img class="card-img w-100 d-block shadow" alt="Default Template" src="abc.jpg">
    <div class="card-img-overlay">
        <a class="btn btn-dark btn-sm template-preview float-end" role="button" target="_blank" href="organization.html/template.html?id=1">Preview</a>
    </div>
    <div class="card-body">
      <h5 class="card-title">Default Template</h5>
      <p class="card-text text-success">Owned</p>
    </div>
  </div>
</div>

I'm doing the sorting via a <select /> element where the <option /> values is a JSON literal that hold the field and direction, e.g.:

{
  "field": "id",
  "direction": "asc"
}

In the <select />'s change event, I have the following:

const templateSort_Change = (e) => {
    const templateSort = e.currentTarget;
    const templateSortValue = templateSort.value;
    const sort = JSON.parse(templateSortValue);
    const cols = document.querySelectorAll('div.col');
    const sortedCols = Array.from(cols).sort((a, b) => {
        if (sort.direction === 'desc') {
            return a.dataset[sort.field].toLowerCase() - b.dataset[sort.field].toLowerCase();
        } else {
            return b.dataset[sort.field].toLowerCase() - a.dataset[sort.field].toLowerCase();
        }
    });
    for (let col of cols) {
        col.parentNode.appendChild(col);
    }
}

The issue is that when it goes to do the for/of loop, the result of cols is the same order every time and looking at the documentation I'm not exactly sure why. I get that I'm doing string comparison, but these ids are still single digits (e.g. 1, 2, 3, etc.).

What am I doing wrong in my sort function that is causing the array to return the same order every time?

3 Answers

For sorting strings you need localeCompare method, the minus ( - ) for sorting numbers not string.

  const sortedCols = Array.from(cols).sort((a, b) => {
    if (sort.direction === 'desc') {
        return b.dataset[sort.field].localeCompare(a.dataset[sort.field])
    } else {
        return a.dataset[sort.field].localeCompare(b.dataset[sort.field);
    }
});

When you do string comparison with -, it will return you a NaN.

console.log('abc' - 'abd');
console.log('a' - 'b');

What you want to do is to use localeCompare().

console.log('abc'.localeCompare('abd'));
console.log('abd'.localeCompare('abc'));
console.log('a'.localeCompare('b'));
console.log('b'.localeCompare('a'));

A few issues: First, your asc/desc sorting was backward, so I've fixed that. Second, most important, your for (let col of cols) loop should iterate over sortedCols, not cols. Third, I'm not sure how you intended to update the DOM but I've done it by adding a wrapper <div> to all your cols.

const templateSort_Change = (e) => {
    const templateSort = e.currentTarget;
    const templateSortValue = templateSort.value;
    const sort = JSON.parse(templateSortValue);
    const cols = document.querySelectorAll('div.col');
    const sortedCols = Array.from(cols).sort((a, b) => {
        if (sort.direction === 'asc') {
            return a.dataset[sort.field].toLowerCase() - b.dataset[sort.field].toLowerCase();
        } else {
            return b.dataset[sort.field].toLowerCase() - a.dataset[sort.field].toLowerCase();
        }
    });
    document.getElementById('container').innerHTML = '';
    
    for (let col of sortedCols) {
        document.getElementById('container').appendChild(col);
    }
}

document.querySelector('select').addEventListener('change', templateSort_Change);
<div id="container">
    <div class="col mb-3" data-id="1" data-filter="Owned">
      <div class="card template">
        <img class="card-img w-100 d-block shadow" alt="Default Template" src="abc1.jpg">
        <div class="card-img-overlay">
            <a class="btn btn-dark btn-sm template-preview float-end" role="button" target="_blank" href="organization.html/template.html?id=1">Preview</a>
        </div>
        <div class="card-body">
          <h5 class="card-title">Default Template 1</h5>
          <p class="card-text text-success">Owned 1</p>
        </div>
      </div>
    </div>
    <div class="col mb-3" data-id="2" data-filter="Owned">
      <div class="card template">
        <img class="card-img w-100 d-block shadow" alt="Default Template" src="abc2.jpg">
        <div class="card-img-overlay">
            <a class="btn btn-dark btn-sm template-preview float-end" role="button" target="_blank" href="organization.html/template.html?id=1">Preview</a>
        </div>
        <div class="card-body">
          <h5 class="card-title">Default Template 2</h5>
          <p class="card-text text-success">Owned 2</p>
        </div>
      </div>
    </div>
    <div class="col mb-3" data-id="3" data-filter="Owned">
      <div class="card template">
        <img class="card-img w-100 d-block shadow" alt="Default Template" src="abc3.jpg">
        <div class="card-img-overlay">
            <a class="btn btn-dark btn-sm template-preview float-end" role="button" target="_blank" href="organization.html/template.html?id=1">Preview</a>
        </div>
        <div class="card-body">
          <h5 class="card-title">Default Template 3</h5>
          <p class="card-text text-success">Owned 3</p>
        </div>
      </div>
    </div>
    <div class="col mb-3" data-id="4" data-filter="Owned">
      <div class="card template">
        <img class="card-img w-100 d-block shadow" alt="Default Template" src="abc4.jpg">
        <div class="card-img-overlay">
            <a class="btn btn-dark btn-sm template-preview float-end" role="button" target="_blank" href="organization.html/template.html?id=1">Preview</a>
        </div>
        <div class="card-body">
          <h5 class="card-title">Default Template 4</h5>
          <p class="card-text text-success">Owned 4</p>
        </div>
      </div>
    </div>
</div>

<select>
<option value='{
  "field": "id",
  "direction": "asc"
}'>Asc</option>
<option value='{
  "field": "id",
  "direction": "desc"
}'>Desc</option>
</select>

Related