Sort tables (almost) instantly with Ruby on Rails and StimulusReflex18 Sep 2021
Today we’re going to use Ruby on Rails and StimulusReflex to build a table that sorts itself each time a user clicks on a header column.
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:
This article will be most useful to folks who are familiar with Ruby on Rails, but 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!
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 and skip ahead to the Building the Table section. The
main branch of the 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:
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:
And then update
Next we’ll update
And then include that stylesheet in
Finally, update the application layout to include the stylesheet generated by webpack:
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:
And fill that partial in with:
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:
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
And then, from your terminal:
Now start up your Rails server and head to localhost:3000/players.
If all has gone well, you should see a table populated with 100 randomly generated players.
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 with a single action,
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:
This generator creates both a
app/reflexes and a related Stimulus controller in
For this article, we won’t make any modifications to the Stimulus controller, so we’ll focus on the Ruby reflex found at
Fill that reflex in with:
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.
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
For our purposes, we care about two data elements that don’t yet exist in the DOM —
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 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.
players partial, update the header row like this:
Here we’ve updated each header cell with the three data attributes we need for the reflex to be triggered and to run successfully.
data-reflex is used to tell StimulusReflex that this element should trigger a reflex when some action occurs. In our case, it will be called on
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
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.
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
Once the value of
players is set by the database query, we use a
selector morph 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
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:
We can fix this by updating the query slightly:
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.
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, Rails will raise an error automatically if anything but a table/column name + a sort direction are passed in to
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:
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
To do this, we’re going to take advantage of CableReady’s tight integration with StimulusReflex. We’ll update the
sort reflex to include a
cable_ready operation that changes the
direction data attribute of the
element that triggered the reflex.
To do this, update
TableReflex like this:
Here we’ve added two private methods to the
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 to set the value of the element’s
direction data attribute to the value of
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 is always followed.
First, broadcasted CableReady
operations execute. Next, StimulusReflex
morphs execute. Finally, CableReady
operations that are not
broadcasted execute (that’s our
Because the StimulusReflex morph runs before the CableReady operation, each table header cell has its
direction data attribute reset to
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
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
Since the value of
data-direction is already
asc, if the next sort direction is
set_dataset_property won’t do anything useful.
sort to skip the CableReady operation in that case:
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:
Let’s start with the CSS.
We’ll insert the CSS directly into our main
application.scss file to keep things simple:
We’ll use the base
.sort class along with a dynamic
.sort-desc to display the sort indicator. If you’re interested in how this CSS works, this is a nice introduction to drawing shapes with CSS.
With the CSS ready, next we’ll create a partial to render the indicator, from your terminal:
And fill that in with:
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
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 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:
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.
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.
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.
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.
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 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 for thoughtfully designed solutions in StimulusReflex, including filterable for working with complex sorting and filtering requirements
- Join the StimulusReflex discord and connect with other folks building cool stuff with StimulusReflex, CableReady, and Rails
That’s all for today, as always, thanks for reading!