Photo by UX Indonesia on Unsplash
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
We will pass a certain sort key via a query parameter to the backend.
A query parameter for the sorting direction will be provided as a query parameter too.
This won't allow you to sort according to multiple sorting criteria.
You won't be able to have multiple sortable tables on one page with this approach. This would require further tweaking.
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
.
applySort(sortKey: string)
as part of the public API will take care of changing the URL parametersort_key
and toggling the query parametersort_order
depending on its current value.we will provide a factory method
fromWindowUrl()
to make aSorting
Object from the currentwindow.location.href
string.sort_order
can be eitherASC
orDESC
.
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_key
s and sort_order
s.
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.