Wrapping a generic sort_table javascript function in a stimulus controller
Tags: javascript, rails, stimulus
Published: 2022-02-03 02:48:22

Several year ago I decided I needed to add a table sort feature to an app. Now there are hundred of "sort_table" functions out there. Some ranging from 50 lines of code to a couple hundred.

I'm not into javascript but get by. I may have even been using CoffeeScript at the time. I picked one that I almost understood that was short and sweet. It did a basic table sort and even offered an ascending/descending option. I'm sorry but I've lost track of were I found this JS so I can't point to the source, but maybe someone can. At some point, probably early 2020 I wrapped this in a Stimulus controller and have used it ever since.

One of my new uses had a table that had a `numeric` column and it didn't handle it well. I went looking for another `sort_table` function and found a few, but I was too lazy to try to wrap the function(s) into a Stimulus controller. I already had something that worked, I just had to modify it by adding a simple numeric switch that I found in one of the searches.

So we'll start with the markup of one of my tables using slim.

```slim
table.small-table id="current_members"
  thead[data-controller="sortTable"]
    tr
      th Del
      th[data-action="click->sortTable#sortBy" data-stype="T"]
        i.fas.fa-sort.noprint
        |Name
      th.numeric[data-action="click->sortTable#sortBy" data-stype="N"]
        i.fas.fa-sort.noprint
        |RQ

      th Tee
      th[data-stype="N" data-action="click->sortTable#sortBy"]
        i.fas.fa-sort.noprint
        |TM

  -  @game.current_players_name.each do |p|
    tr[data-schedule-target="currentPlayers"]
      td = check_box_tag "deleted[]", value=p.id,nil,data:{action:'schedule#update_player'}
      td = p.name
      td = p.player.rquota_limited
      td.bg-green-100.select = select_tag 'tee[]', tee_options(@game.group,p,true), class:"border border-green-500 select",data:{action:'schedule#update_player'}
      td = p.team
```

I add the `sortTable` controller to the table and `data-action="click->sortTable#sortBy"` to any TH column I want to sort. There is also a little css and font awesome icons that are optional. There is also another controller actions.

The `numeric` option is triggered by add a `numeric` class to the TH.

Now the Stimulus controller, again 90% of code I copied from somewhere:

```javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ ]

  connect() {
    this.numeric = false
  }

  sortBy(){
    const th = event.target
    this.numeric = th.classList.contains('numeric')
    const tr = th.closest('tr')
    const table = tr.closest('table')
    var idx = Array.from(tr.children).indexOf(th)
    this.sortTable(table,idx)
  }

  sortTable(table,idx) {
    var  rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
    switching = true;
    // Set the sorting direction to ascending:
    dir = "asc"; 
    /* Make a loop that will continue until
    no switching has been done: */

    while (switching) {
      // Start by saying: no switching is done:
      switching = false;
      rows = table.rows;
      /* Loop through all table rows (except the
      first, which contains table headers): */

      for (i = 1; i < (rows.length - 1); i++) {
        // Start by saying there should be no switching:
        shouldSwitch = false;
        /* Get the two elements you want to compare,
        one from current row and one from the next: */

        x = rows[i].getElementsByTagName("TD")[idx];
        y = rows[i + 1].getElementsByTagName("TD")[idx];
        // Added this check if there is a row that has a colspan e.g. ending balance row
        if ((x == undefined) || (y == undefined)){continue}
        /* Check if the two rows should switch place,
        based on the direction, asc or desc: */

        // Check if numeric sort (th has class numeric) added by ME
        if (!this.numeric) {
          var compx = x.innerHTML.toLowerCase()
          var compy = y.innerHTML.toLowerCase()
        }else{
          var compx = /\d/.test(x.innerHTML) ? parseFloat(x.innerHTML) : 0 
          var compy = /\d/.test(y.innerHTML) ? parseFloat(y.innerHTML) : 0 
        }
        
        if (dir == "asc") {
          if (compx > compy) {
            // If so, mark as a switch and break the loop:
            shouldSwitch = true;
            break;
          }
        } else if (dir == "desc") {
          if (compx < compy) {
            // If so, mark as a switch and break the loop:
            shouldSwitch = true;
            break;
          }
        }
      }
      if (shouldSwitch) {
        /* If a switch has been marked, make the switch
        and mark that a switch has been done: */
        rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
        switching = true;
        // Each time a switch is done, increase this count by 1:
        switchcount ++; 
      } else {
        /* If no switching has been done AND the direction is "asc",
        set the direction to "desc" and run the while loop again. */
        if (switchcount == 0 && dir == "asc") {
          dir = "desc";
          switching = true;
        }
      }
    }
  }

}
```

I'm sure this is an old fashioned `bubble sort` and there maybe better functions, but it worked for me. I just wanted to point out it's not that hard to wrap a JS function into Stimulus.