Rails 7.1.x Dynamic Tables
Simple Dynamic Tables without Javascript
I recently learned that Rails 7.1+ has some delightful features that make it easy to render dynamic tables without JavaScript. This is an exploration of using these new features.
This code can be found at: https://github.com/btihen-dev/rails_dynamic_table
Getting Started
I will create a basic application - using the starting point in the Basic App found at the repo.
In the case I will use the following options:
rails _7.1.3.2_ new dynamic_tables -T --database=postgresql --css=bootstrap
cd dynamic_tables
NOTE: So far this code only works when using import maps
and not when using esbuild
. When I find a solution I will update this article or write a new one about these features with esbuild.
Dynamic Tables
Let’s convert the people index
view into a table view app/views/people/index.html.erb
# app/views/characters/index.html.erb
<p style="color: green"><%= notice %></p>
<% content_for :title, "Characters" %>
<div class="container text-center">
<div class="row justify-content-start">
<div class="col-9">
<h1>Characters</h1>
<table class="table table-striped table-hover">
<thead class="sticky-top">
<tr class="table-primary">
<th scope="col">
ID
</th>
<th scope="col">
First Name
</th>
<th scope="col">
Last Name
</th>
<th scope="col">
Gender
</th>
<th scope="col">
Species
</th>
<th scope="col">
Company-Job
</th>
</tr>
</thead>
<tbody class="scrollable-table">
<div id="characters">
<% @characters.each do |character| %>
<tr id="<%= dom_id(character) %>">
<th scope="row"><%= link_to "#{character.id}", edit_character_path(character) %></th>
<td><%= character.first_name %></td>
<td><%= character.last_name %></td>
<td><%= character.gender %></td>
<td><%= character.species.species_name %></td>
<td class="text-start">
<ul class="list-unstyled">
<% character.person_jobs.each do |person_job| %>
<li>
<b><%= person_job.job.company.company_name %></b><br>
- <%= person_job.job.role %><br>
<em>
(<%= person_job.start_date.strftime("%e %b '%y") %> -
<%= person_job.end_date&.strftime("%e %b '%y") || 'present' %> )
</em>
</li>
<% end %>
</ul>
</td>
</tr>
<% end %>
</div>
</tbody>
</table>
</div>
<div class="col-3">
<%= link_to "New", new_character_path, class: "mt-5 sticky-top btn btn-primary" %>
</div>
</div>
</div>
start rails:
bin/rails s -p 3030
go to: http://localhost:3030/people
and be sure this table looks reasonable.
now let’s commit this.
git add .
git commit -m "basic people table added"
Sortable Columns
We want sortable columns without a page reload (so open your developer window and open the network so you can see how fast and little data is transmitted)
Let’s add a sort link helper method to our app/helpers/people_helper.rb
# app/helpers/characters_helper.rb
module CharactersHelper
def sort_link(column:, label:)
link_to(label, characters_path(column: column))
end
end
Let’s update the people index controller app/controllers/people_controller.rb
to use this new helper method.
# app/controllers/people_controller.rb
def index
query =
Character
.includes(:species)
.includes(person_jobs: [ job: :company ])
if params[:column].present?
@characters = query.order("#{params[:column]}").all
else
@characters = query.all
end
end
Let’s update the people index
with our new sort methods.
# app/views/people/index.html.erb
<thead class="sticky-top">
<tr class="table-primary">
<th scope="col">
<%= sort_link(column: "id", label: "Id") %>
</th>
<th scope="col">
<%= sort_link(column: "first_name", label: "First Name") %>
</th>
<th scope="col">
<%= sort_link(column: "last_name", label: "Last Name") %>
</th>
<th scope="col">
<%= sort_link(column: "gender", label: "Gender") %>
</th>
<th scope="col">
<%= sort_link(column: "species.species_name", label: "Species") %>
</th>
<th scope="col">
Company-Job
</th>
</tr>
</thead>
This is a cool proof of concept, but unfortunately only sorts into ascending and not yet descending.
Bi-directional Sort
we update the helper with:
# app/helpers/characters_helper.rb
module CharactersHelper
def sort_link(column:, label:)
direction = column == params[:column] ? next_direction : 'asc'
link_to(label, characters_path(column: column, direction: direction))
end
def next_direction = params[:direction] == 'asc' ? 'desc' : 'asc'
def sort_indicator = tag.span(class: "sort sort-#{params[:direction]}")
def show_sort_indicator_for(column)
sort_indicator if params[:column] == column
end
end
and the controller with:
# app/controllers/characters_controller.rb
def index
query = Character
.includes(:species)
.includes(person_jobs: { job: :company })
if params[:column].present?
# @characters = query.order("#{params[:column]}").all
@characters = query.order("#{params[:column]} #{params[:direction]}").all
else
@characters = query.all
end
end
this is pretty cool, but with every refresh we reset our scroll location, which is annoying when navigating a long table.
Adding Sort Arrows
# app/helpers/characters_helper.rb
module CharactersHelper
def sort_link(column:, label:)
direction = column == params[:column] ? future_direction : 'asc'
link_to(label, characters_path(column: column, direction: direction))
end
def future_direction = params[:direction] == 'asc' ? 'desc' : 'asc'
def sort_arrow
case params[:direction]
when 'asc' then tag.i(class: "bi bi-arrow-up")
when 'desc' then tag.i(class: "bi bi-arrow-down")
else tag.i(class: "bi bi-arrow-down-up")
end
end
def sort_arrow_for(column)
params[:column] == column ? sort_arrow : tag.i(class: "bi bi-arrow-down-up")
end
end
now let’s update the view too:
# app/views/people/index.html.erb
<thead class="sticky-top">
<tr class="table-primary">
<th scope="col">
<%= sort_link(column: "id", label: "Id") %>
<%= sort_arrow_for("id") %>
</th>
<th scope="col">
<%= sort_link(column: "first_name", label: "First Name") %>
<%= sort_arrow_for("first_name") %>
</th>
<th scope="col">
<%= sort_link(column: "last_name", label: "Last Name") %>
<%= sort_arrow_for("last_name") %>
</th>
<th scope="col">
<%= sort_link(column: "gender", label: "Gender") %>
<%= sort_arrow_for("gender") %>
</th>
<th scope="col">
<%= sort_link(column: "species.species_name", label: "Species") %>
<%= sort_arrow_for("species.species_name") %>
</th>
<th scope="col">
Company-Job
</th>
</tr>
</thead>
Fix Scroll Reset
To fix scroll reset we need to enable new Turbo 8 features - morph dom and keeping scroll location:
# app/views/layouts/application.html.erb
<head>
...
<!-- adds morph-dom to rails and fixes scroll reset -->
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= yield :head %>
...
</head>
you can add this feature more surgically, this enables these features everywhere.
Now we need to inform our helper about the morph-dom by adding data: { turbo_action: 'replace' }
to our path so the helper now looks
# app/helpers/characters_helper.rb
module CharactersHelper
def sort_link(column:, label:)
direction = column == params[:column] ? future_direction : 'asc'
link_to(label, characters_path(column: column, direction: direction), data: { turbo_action: 'replace' })
end
def future_direction = params[:direction] == 'asc' ? 'desc' : 'asc'
def sort_arrow
case params[:direction]
when 'asc' then tag.i(class: "bi bi-arrow-up")
when 'desc' then tag.i(class: "bi bi-arrow-down")
else tag.i(class: "bi bi-arrow-down-up")
end
end
def sort_arrow_for(column)
return sort_arrow if params[:column] == column
tag.i(class: "bi bi-arrow-down-up")
end
end
now when you sort a column it doesn’t reset the scroll location.
Add Search Filter
https://www.colby.so/posts/filtering-tables-with-rails-and-hotwire
Resources
Adding Bootstrap to an existing project
- https://www.youtube.com/watch?v=phOUsR0dm5s
- https://medium.com/@gjuliao32/installing-bootstrap-rails-7-a-step-by-step-guide-0fc4a843d94f
Bootstrap Icond
Rails Table Articles
- Table Sorting Rails 7.1 - 21 Mar 2024
- Table Filtering Rails 7.0 - 15 Oct 2021
- Table Sorting Rails 7.0 - 19 Sep 2021
- Table Sorting with Stimulus
Rails Hotwire
- Turbo Rails Intro
- Hotwiring Rails Book
- Hot Rails Tutorial - building Turbo Rails
- Rebuilding Turbo Rails - new version
- Rails Hotwire Modals
- Turbo Frame Pages in Ruby on Rails 7
- Digging into Turbo with Ruby on Rails 7
- Mastering Turbo Frames and Turbo Streams in Rails 7: Build a Journal Entry Tagging Feature
- Odin Probject - Turbo Tutorial
- Hotwire Turbo Transitions