Sort tables (almost) instantly with Ruby on Rails and StimulusReflex
Tags: ruby, rails, webdev, tutorial
Published: 2021-09-19 14:03:38

Today we’re going to use Ruby on Rails and [StimulusReflex](https://docs.stimulusreflex.com/) to build a table that sorts itself each time a user clicks on a header column.

Sorting will occur without a page turn, in less than 100ms, won't require any custom JavaScript, and we'll build the whole thing with regular old ERB templates and a little bit of Ruby. 

The end result will be very fast, efficient, simple to reason about, and easy to extend as new functionality is required.

It'll be pretty fancy.

When we're finished, it will work like this:

![A screen recording of a user clicking on column headers on a data table. With each click, the table sorts itself by that column and a triangle indicator appears next to the column used for sorting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwitj4pcuj71mmslm5y8.gif)

You can view the finished product [on Heroku](https://stark-peak-59471.herokuapp.com/), or find the full source on [Github](https://github.com/DavidColby/sortable_players/tree/sortable).

This article will be most useful to folks who are familiar with Ruby on Rails, you will not need any previous experience with Stimulus or StimulusReflex to follow along. If you’ve never worked with Rails before, some concepts here may be a little tough to follow.

Let’s get started!

## Setup
As usual, we’ll start with a brand new Rails 6.1 application, with Tailwind and StimulusReflex installed. Tailwind is not a requirement, but it helps us make our table look a little nicer and the extra setup time is worth the cost.

If you’d like to skip the copy/pasting setup steps, you can clone down the example [repo](https://github.com/DavidColby/sortable_players/tree/main) and skip ahead to the [Building the Table section](#building-the-table). The `main` branch of the example repo is pinned to the end of the setup process and ready for you to start writing code.

If you’re going to follow along with the setup, first, from your terminal:

```
rails new player_sorting -T
cd player_sorting
bundle add stimulus_reflex
bundle add faker
rake stimulus_reflex:install
rails g model Team name:string
rails g scaffold Player name:string team:references seasons:integer
rails db:migrate
```

Assuming you’ve got Rails and Yarn installed, this will produce a brand new Rails 6.1 application (at the time of this writing), install StimulusReflex, and scaffold up the Team and Player models that we’ll use to build our sortable table.

If you don’t care to use Tailwind for this article, feel free to skip past this next section, Tailwind is a convenient way to make things look presentable, but if you just want to focus on sorting the table without any styling, Tailwind is not necessary!

If you want to install Tailwind, start in your terminal:

yarn add tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init
mkdir app/javascript/stylesheets
touch app/javascript/stylesheets/application.scss

And then update `tailwind.config.js`:

```javascript
module.exports = {
  purge: [
    './app/**/*/*.html.erb',
    './app/helpers/**/*/*.rb',
    './app/javascript/**/*/*.js',
    './app/javascript/**/*/*.vue',
    './app/javascript/**/*/*.react'
  ],
  darkMode: false,
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}
```

And postcss.config.js:

```javascript
module.exports = {
  plugins: [
    require("tailwindcss")("./tailwind.config.js"),
    require("postcss-import"),
    require("postcss-flexbugs-fixes"),
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009",
      },
      stage: 3,
    }),
  ],
}
```

Next we’ll update `app/javascripts/stylesheets/application.scss` to import Tailwind:

```scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```

And then include that stylesheet in `app/javascripts/packs/application.js`:

```javascript
import "../stylesheets/application.scss"
```

Finally, update the application layout to include the stylesheet generated by webpacker:

```
<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload', media: 'all' %>
```

Whew. We’re through the setup and ready to start building.

## Building the table
First up, let’s get our bearings.

The core of our application is the `Players` resource. We are going to construct a table that displays all of the players in our database, with the players name, their team’s name, and their seasons played as columns. 

We’ll only use the `Team` model created during setup in the context of `Players`, so we don’t need a controller or views for teams.

We’ll start by moving the table from the players index view to a partial. From your terminal:

```
touch app/views/players/_players.html.erb
```

And fill that partial in with:

```erb
<div id="players" class="shadow overflow-hidden rounded border-b border-gray-200">
  <table class="min-w-full bg-white">
    <thead class="bg-gray-800 text-white">
      <tr>
        <th id="players-name" class="text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer">
          <span>Name</span>
        </th>
        <th id="players-team" class="text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer">
          <span>Team</span>
        </th>
        <th id="players-seasons" class="text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer">
          <span>Seasons</span>
        </th>
      </tr>
    </thead>

    <tbody class="text-gray-700">
      <% players.each do |player| %>
        <tr>
          <td class="text-left py-3 px-6"><%= player.name %></td>
          <td class="text-left py-3 px-6"><%= player.team.name %></td>
          <td class="text-left py-3 px-6"><%= player.seasons %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
```

Most of the markup here is Tailwind classes for styling the table.

The functionally important pieces are the `ids` set on the wrapper div (`#players`) and the ids set on the table header cells. These ids will be used later to update the DOM when the user clicks to sort the table.

Next update the index view to use the new partial:

```erb
<div class="max-w-7xl mx-auto mt-12">
  <%= render "players", players: @players %>
</div>
```

With these changes in place, we have a nice looking table ready to display the players in the database, but we don’t have any players. Since we’re going to be sorting, let’s make sure the database has plenty of data in it.

Copy this into `db/seeds.rb`:

```ruby
['Dallas Mavericks', 'LA Clippers', 'LA Lakers', 'San Antonio Spurs', 'Boston Celtics', 'Miami Heat', 'New Orleans Pelicans'].each do |name|
  Team.create(name: name)
end

100.times do
  Player.create(name: Faker::Name.name, team: Team.find(Team.pluck(:id).sample), seasons: rand(25))
end
```

And then, from your terminal:

```
rails db:seed
```

Now start up your Rails server and head to [localhost:3000/players](http://localhost:3000/players).

If all has gone well, you should see a table populated with 100 randomly generated players.

![A screenshot of a data table with columns for name, team, and seasons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2f73dyit8vhlz18bloha.png)

Next up, we’ll build our sorting mechanism with StimulusReflex.

## Sorting with StimulusReflex
To sort the table, we’re going to create a [reflex class](https://docs.stimulusreflex.com/rtfm/reflex-classes) with a single action, `sort`.

When the user clicks on a table header, we’ll call the reflex action to sort the players and update the DOM.

We’ll start by generating a new Reflex. From your terminal:

```
rails g stimulus_reflex Table
```

This generator creates both a `reflex` in `app/reflexes` and a related Stimulus controller in `app/javascripts/controllers`.

For this article, we won’t make any modifications to the Stimulus controller. Instead, we’ll focus on the reflex found at `app/reflexes/table_reflex.rb`

Fill that reflex in with:

```ruby
class TableReflex < ApplicationReflex
  def sort
    players = Player.order("#{element.dataset.column} #{element.dataset.direction}")
    morph '#players', render(partial: 'players', locals: { players: players })
  end
end
```

The first line of `sort` is a standard Rails ActiveRecord query. In it, we retrieve all of the players from the database, ordered by attributes sent from the DOM when the reflex action is triggered.

Reflex actions have access to a [variety of properties](https://docs.stimulusreflex.com/rtfm/reflex-classes#building-your-reflex-action). In our case, the property we’re interested in is [element](https://docs.stimulusreflex.com/rtfm/reflex-classes#element).

`element` is a representation of the DOM element that triggered the reflex and it includes all of the data attributes set on that element, accessible via `element.dataset`.

This means that in reflex actions, we can always access data attributes from the element that triggered the reflex as if we were working with that element in JavaScript. Handy.

For our purposes, we care about two data elements that don’t yet exist in the DOM — `column` and `direction`. The ActiveRecord query to retrieve and order players uses those values to know which column to order the results by, and in which direction (ascending or descending) the results should be ordered.

After we’ve retrieved the ordered list of players from the database, we use a [selector morph](https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs) to update the DOM, replacing the content of the `players` partial we created earlier with the updated list of players.

Our reflex is built, but there’s no way for a user to trigger the reflex. Let’s add that next.

In the `players` partial, update the header row like this:

```erb
<tr>
  <th 
    id="players-name" 
    class="text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"
    data-reflex="click->Table#sort"
    data-column="name"
    data-direction="asc"
  >
    <span>Name</span>
  </th>
  <th 
    id="players-team" 
    class="text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"
    data-reflex="click->Table#sort"
    data-column="teams.name"
    data-direction="asc"
  >
    <span>Team</span>
  </th>
  <th 
    id="players-seasons" 
    class="text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer"
    data-reflex="click->Table#sort"
    data-column="seasons"
    data-direction="asc"
  >
    <span>Seasons</span>
  </th>
</tr>
```

Here we’ve updated each header cell with the three data attributes we need for the reflex to be triggered and to run successfully.

First, `data-reflex` is used to [tell StimulusReflex](https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes) that this element should trigger a reflex when some action occurs. In our case, it will be called on `click`.

Each cell also gets a unique `data-column` value, which we use to sort the players result by the matching database column. Finally, each header cell also starts with a `direction` of `asc` which is used to set the direction of the order query.

Let’s look at the Reflex code again and review what’s happening now that we’ve updated the DOM to call this reflex.

```ruby
def sort
  players = Player.order("#{element.dataset.column} #{element.dataset.direction}")
  morph '#players', render(partial: 'players', locals: { players: players })
end
```

The `sort` method we’ve defined matches the name of the `data-reflex` on each header cell. When the header cell is clicked, this reflex will run.

 The header cell that that the user clicks will be passed to the reflex as `element`, giving us access to the element’s data attributes, which we access through `element.dataset`.

Once the value of `players` is set by the database query, we use a `selector` [morph](https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs) to tell the browser to update the element with the id of `players` with the content of the `players` partial, using the updated, reordered list of players.

This is the magic of StimulusReflex in action. With just a couple of data attributes and a few lines of simple Ruby code, our users can now click on a table header and, in < 100ms, they’ll see a table sorted to match their request.

Refresh the page and try it out for yourself. If all has gone well, clicking on a header cell should sort the table by that column in ascending order. While this is nice, we have a few more items to address before our work is complete.

Next up, we’ll address an error with the order query, and then finish this article by modifying the sort reflex to allow users to sort in both ascending and descending order and display visual feedback to indicate what column is being sorted.

## Fixing an ordering error
First, sharp eyed readers might have noticed that sorting by the team name column doesn’t work yet.

Each header cell’s `column` data attribute matches a column in the database, so we can generate the order query dynamically. This works fine for the name and season because those columns live on the `Players` table. ActiveRecord knows how to order by `name` and `seasons` without any extra effort.

For the team name column, we’re passing `teams.name` to the `order` call in our query, which ActiveRecord trips over with an error like this:

```
# The text will vary depending on the database adapter you're using!
Reflex Table#sort failed: SQLite3::SQLException: no such column: teams.name
```

We can fix this by updating the query slightly:

```ruby
players = Player.includes(:team).order("#{element.dataset.column} #{element.dataset.direction}")
```

Here we added `includes(:team)` to the existing order query, making the `Teams` table accessible in the order clause and fixing the “no such column” error that was thrown when attempting to sort by team name.

Note that `joins` instead of `includes` would also fix the error, but since we need to use team name when we render the players partial (to display each player’s team), `includes` is the better choice.

Before moving on, you’ll notice that we are using user-accessible values to generate a SQL query — anyone can modify data-attributes in their browser’s dev tools.

Prior to Rails 6, this could have opened up our application to SQL injection; however, [since Rails 6](https://github.com/rails/rails/pull/27947), Rails will raise an error automatically if anything but a table/column name + a sort direction are passed in to `order`.

## Adding descending ordering
With the work we've done so far, sorting the table works great as long as the user only wants to sort in ascending (A - Z) order. Since the sort direction is read from a data attribute that is always “asc”, there is no way to sort in descending (Z - A) order. Let's add that functionality next.

Before jumping in to the code, let’s outline the desired user experience. 

When a user clicks on a column header, the table should be sorted by that column, in ascending order. When the user clicks on the same column header again, the table should be sorted by that column in descending order. And then we alternate, forever, between ascending and descending on subsequent clicks. 

Sorting by a different column should always sort in ascending order on the first click.

Here’s a gif of what we’re aiming for:

![A screen recording of a user clicking on column headers to sort a data table in ascending and descending order](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wp9fp6x6v1b80m8os8r8.gif)

To achieve the desired user experience, we need to track the next sort direction for each header cell, so that when the `sort` reflex is called, the right direction can be sent to the `order` query.

To do this, we’re going to take advantage of [CableReady’s](https://docs.stimulusreflex.com/rtfm/cableready) tight integration with StimulusReflex. We’ll update the `sort` reflex to include a `cable_ready` [operation](https://cableready.stimulusreflex.com/reference/operations) that changes the `direction` data attribute of the `element` that triggered the reflex.

To do this, update `TableReflex` like this:

```ruby
def sort
  # snip
  set_sort_direction
end

private

def next_direction(direction)
  direction == 'asc' ? 'desc' : 'asc'
end

def set_sort_direction
  cable_ready
    .set_dataset_property(
      selector: "##{element.id}",
      name: 'direction',
      value: next_direction(element.dataset.direction)
    )
end
```

Here we’ve added two private methods to the `TableReflex` class. `next_direction` is a simple helper method that takes the current value of direction and returns the next value.

`set_sort_direction` is more interesting. In it, we use CableReady’s `set_dataset_property` [operation](https://cableready.stimulusreflex.com/v/v5/reference/operations/element-mutations#set_dataset_property) to set the value of the element’s `direction` data attribute to the value of  `next_direction` .

Finally, we call `set_sort_direction` in the `sort` reflex, which adds the `set_dataset_property` operation to the queue each time the `sort` reflex runs.

With this in place, refresh and click on the same column multiple times to see that each click reorders the table, toggling between ascending and descending order.

## Order of operations: Not just for math class

Before moving on, it is important to pause and think about how this code works. When a reflex includes CableReady operations, a [specific order of operations](https://docs.stimulusreflex.com/rtfm/cableready#order-of-operations) is always followed.

First, [broadcasted](https://cableready.stimulusreflex.com/reference/methods) CableReady `operations` execute. Next, StimulusReflex `morphs` execute. Finally, CableReady `operations` that are not `broadcasted` execute (that’s our `set_dataset_property` operation).

Because the StimulusReflex morph runs before the CableReady operation, each table header cell has its `direction` data attribute reset to `asc` when `sort` is triggered. This behavior lets us “reset” sort directions when moving between columns without having to add logic in the partial.

Immediately **after** the StimulusReflex morph, `set_dataset_property` runs and updates the value of `direction` on the currently active sort column.

If we appended `.broadcast` to the `set_dataset_property` operation, the direction property would be updated **before** the StimulusReflex `morph`, causing the CableReady update to be overwritten by the `morph`, breaking the ability to sort in descending order.

This order of operations is important to understand, and helps unlock a new level of functionality within reflexes.

Before moving on, now that we understand the order of operations in reflex actions, we can use that knowledge to make a small optimization to the `sort` reflex.

We know that every time the reflex runs, each header cell will have its `data-direction` value set to `asc` before the active sort column is updated by the CableReady `set_dataset_property` operation.

Since the value of `data-direction` is already `asc`, if the next sort direction is `asc`, `set_dataset_property` won’t do anything useful. 

Let’s update `sort` to skip the CableReady operation in that case:

```ruby
def sort
# snip
  set_sort_direction if next_direction(element.dataset.direction) == 'desc'
end
```

Now `set_sort_direction` will only be run when necessary, simplifying our DOM updates at the cost of slightly more complexity in our ruby code.

Let’s finish up our sortable table implementation by adding a visual indicator to the table when sorting is active.

## Sort direction visuals
To indicate which column is being sorted, and in which direction, we’ll draw a triangle with CSS, with an upward pointing triangle indicating ascending order, and a downward triangle indicating descending order.

Only the column that is being used for sorting will display the icon.

When we’re finished, the indicator will look like this:

![A screenshot of a data table with a triangle pointing upward positioned to the left of a column header labeled Team, indicating the table is sorted in ascending order by team name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n0f3lgl6moc2nb8pdq2q.png)

Let’s start with the CSS.

We’ll insert the CSS directly into our main `application.scss` file to keep things simple:

```css
.sort {
  position: absolute;
  top: 5px;
  left: -1rem;
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
}

.sort-desc {
  border-top: 8px solid #fff;
}

.sort-asc {
  border-bottom: 8px solid #fff;
}
```

We’ll use the base `.sort` class along with a dynamic `.sort-asc` or `.sort-desc` to display the sort indicator. If you’re interested in how this CSS works, [this is a nice introduction](https://www.freecodecamp.org/news/css-shapes-explained-how-to-draw-a-circle-triangle-and-more-using-pure-css/) to drawing shapes with CSS.

With the CSS ready, next we’ll create a partial to render the indicator, from your terminal:

```
touch app/views/players/_sort_indicator.html.erb
```

And fill that in with:

```erb
<div class="relative">
  <span class="sort sort-<%= direction %>"></span>
</div>
```

Nothing fancy here, just appending the local `direction` variable to the class name to ensure our triangle points in the right direction.

We’ll finish up by updating the `sort` reflex to insert the sort indicator into the DOM, again relying on a CableReady operation that runs immediately after the StimulusReflex `morph`.

```ruby
class TableReflex < ApplicationReflex
  def sort
    # snip
    insert_indicator
  end

  private

  # snip
  def insert_indicator
    cable_ready
      .prepend(
        selector: "##{element.id}",
        html: render(
          partial: 'players/sort_indicator',
          locals: { direction: element.dataset.direction }
        )
      )
  end
end
```

Here we added another private method to the reflex to handle inserting the sort indicator when the `sort` reflex is called.

 `insert_indicator` uses CableReady’s `prepend` [operation](https://cableready.stimulusreflex.com/reference/operations/dom-mutations#prepend) to insert the content of the `sort_indicator` partial into the DOM, before the target element’s first child.

With this in place, we can refresh the page and see the sort indicator added each time the sort reflex runs, pointing up for ascending sorts and down for descending sorts:

![A screen recording of a user clicking on column headers on a data table. With each click, the table sorts itself by that column and a triangle indicator appears next to the column used for sorting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cks9k338rlj13z03u7o.gif)

## An implementation note

During this article I made a choice to use `cable_ready` operations to add the sort indicator and update the data attribute.

Instead of `cable_ready` operations, another approach would be to assign instance or local variables for things like "active column" and "next direction" during the `sort` reflex. We could then read those variables when rendering the `players` partial, using them to render the sort indicator and set the next sort direction.

This approach would allow us to eliminate the `cable_ready` operations; however, doing so would complicate the view. Either approach is fine, my personal preference is to rely on the very fast `cable_ready` operations to simplify the view.

Using `cable_ready` also has the added benefit of letting us talk more about how StimulusReflex works, which is a bonus in a tutorial article like this one. As you spend more time with StimulusReflex, experiment with different approaches and find what works best for you.

## Wrapping up
Today we built a sortable table interface with Ruby on Rails, StimulusReflex, and CableReady. Our table is fast, updates efficiently, is easy to extend, and is no where near production ready yet. What we built today was part one of a two part series. Next up, we’ll extend the sortable table by adding filtering and pagination, getting closer to the full-featured implementation seen in [Beast Mode](https://beastmode.leastbad.com/).

While there are numerous ways to implement a sortable table interface, for Rails developers, StimulusReflex is worthy of strong consideration. SR’s fast, efficient mechanisms for updating and re-rendering the DOM, including [bypassing ActionDispatch’s overhead](https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs) with `selector` morphs, allow us to sort and render the updated table extremely quickly, with minimal code complexity or additional mental overhead. Its tight integrations with CableReady and Stimulus combine into an extremely powerful tool in any Rails developers kit.

To go further into StimulusReflex and CableReady:
* Review [StimulusReflex patterns](https://www.stimulusreflexpatterns.com/patterns/) for thoughtfully designed solutions in StimulusReflex, including [filterable](https://www.stimulusreflexpatterns.com/patterns/filterable_reflex/) for working with complex sorting and filtering requirements
* Join the [StimulusReflex discord](https://discord.com/invite/stimulus-reflex) and connect with other folks building cool stuff with StimulusReflex, CableReady, and Rails

That’s all for today, as always, thanks for reading!