RoyalZSoftware
Back to Blog

How to build a reusable sortable table in Ruby on Rails with Stimulus

Alexander Panov 2023-03-31 4 min read
How to build a reusable sortable table in Ruby on Rails with Stimulus

This article is all about writing a sortable table with reusable software modules in Stimulus and the Ruby on Rails backend too.

The result and its limitations

Building the module

The module is divided into the frontend Sorting Class, Stimulus Controller and the backend SortBy functionality.

Sorting Class in the frontend

To make the sorting reusable for other tables and other use cases we will encapsulate the following sorting behavior into its own class named Sorting.

The implementation will look like the following:

class Sorting {
    sortKey;
    sortOrder;
 
    constructor(sortKey, sortOrder) {
        this.sortKey = sortKey;
        this.sortOrder = sortOrder;
    }
 
    changeDirection() {
        this.sortOrder = this.sortOrder === 'DESC' ? 'ASC': 'DESC';
    }
 
    applySort(key) {
        const wantsToChangeDirection = this.sortKey?.toLowerCase() === key.toLowerCase();
 
        this.sortKey = key;
        if (wantsToChangeDirection) {
            this.changeDirection();
        } else {
            this.sortOrder = 'DESC';
        }
 
        this.redirectToSortURL();
    }
 
    redirectToSortURL() {
        const url = new URL(document.URL);
        const searchParams = new URLSearchParams(url.search);
 
        // redirect to URL
        searchParams.set("sort_key", this.sortKey);
        searchParams.set("sort_order", this.sortOrder)
        url.search = decodeURIComponent(searchParams.toString());
        window.location.href = url.toString();
    }
 
    static fromWindowUrl() {
        const url = new URL(document.URL);
        const searchParams = new URLSearchParams(url.search);
 
        const sortKey = searchParams.get("sort_key");
        const sortOrder = searchParams.get("sort_order")
 
        return new Sorting(sortKey, sortOrder);
    }
}

Abstracting this behavior into its class will make this snippet easier to understand, less dependent and ultimately easier to test.

Integrating the Sorting Class in a Stimulus Controller

In the connect Stimulus hook we will call the previously created Sorting#fromWindowUrl() factory and saving it as an instance variable of the controller.

// sort_controller.js
import {Controller} from '@hotwired/stimulus';
 
export default class extends Controller {
 
    static targets = [...super.targets ];
 
    connect() {
        this.sorting = Sorting.fromWindowUrl();
    }
 
    sort(event) {
        const sortingKey = event.target.innerHTML;
 
        this.sorting.applySort(sortingKey)
    }
}

You can see the Stimulus Controller is incredibly small and easy to read.

Building the SortBy class in Ruby

The SortBy class exists to neatly encapsulate the sorting behavior. Theoretically one could just put those three lines into the controller but this will make it harder to understand and clutters the codebase if used on multiple pages.

# sort_by.rb
class SortBy
 
  def initialize(params)
    @sort_key = params[:sort_key] || 'sequential_id'
    @sort_order = params[:sort_order] || 'DESC'
  end
 
  def sort(model)
    model.order("#{@sort_key} #{@sort_order}")
  end
end

This is just the minimalistic excerpt of the actual implementation that I have ended up using.

It lacks restriction of invalid sort_keys and sort_orders.

Bringing it all together

HTML Template

Keep an eye on the Stimulus directives.

<table data-controller="sort">
  <thead>
    <tr>
        <% %i[sequential_id amount name size].each do |col| %>
          <th
             data-action="click->sort#sort"
             data-sorting-value="<%= col %>"
          >
            <%= col %>
          </th>
        <% end %>
    </tr>
  </thead>
...
  <tbody>
    <%= items.each do |item| %>
      <tr>
        <td><%= item.sequential_id></td>
        <%# all other cols... #>
      </tr>
    <% end %>
  </tbody>
</table>

Telling the index action to sort

Last but not least you will need to tell the index action to handle the query parameters.

Do this by instantiating a new SortBy object with the parameters of the request and calling the sort(filtered_items) method.

class UserController
    
    def index
        @items = User.all
        @items = SortBy.new(params).sort(@items)
    end
 
end

This might look like an error will be raised if you don't check that the params hash contains the sort key and sort direction, at first. But SortBy will take care of providing default values.

Please also note that this is not the most generic approach, it is a great middle way for implementing the sort fast and extensible.

The original use case is even more specific. That's why there is the sequential_id default value in SortBy.

Recap

If you need to implement a basic table header sorting fast with Ruby on Rails and StimulusJs, I hope I could inspire you with my approach.

This thing comes without any external dependencies, besides Active Record and took me less than 30 minutes to implement.

Thank you for reading this far. I would be very thrilled if you were to share this with a developer friend of yours.

More articles

Building a Web Server in Go: A Beginner's Guide

Building a Web Server in Go: A Beginner's Guide

Oleksandr Vlasov 2024-11-26 3 min read
React Hooks — createContext, useContext, useMemo

React Hooks — createContext, useContext, useMemo

Oleksandr Vlasov 2024-10-23 4 min read
Mastering Git Rebase Interactive: Squashing Commits for a Clean History

Mastering Git Rebase Interactive: Squashing Commits for a Clean History

Alexander Panov 2024-10-21 2 min read