When you have a lot of records, it makes sense to paginate the table. That way, instead of loading thousands of records, you can load a subset, with better performance, and allow your user to move through the pages as leisure. It’s also better for usability, because it’s easier to go back to a particular page rather than scroll through a long list.
I like to use Basecamp’s geared_pagination
gem to manage the pagination, offsets, and current page. I’ve found that when I want to delete a particular record, I could the HOTWire or javascript to remove the individual row. But it wouldn’t update the page counts, or make add in another record. This was a usability problem when I had to delete lots of rows. I would eventually remove all the rows on the page, and then I had to refresh to pull more records from the database.
I realized I can use Turbo Frames to replace the entire table, which will update the page counts, and add data back on to the page. Here is how it works below:
Here’s how it all works. This tutorial assumes Rails 7, and HOTWire configured with the turbo-rails
gem.
The Controller
I’m going to use a Book model, but you can really use any of your records. Here is the books_controller.rb
:
# frozen_string_literal: true
class BooksController < ApplicationController
def index
set_page_and_extract_portion_from Book.all
end
def destroy
@book = Book.find params[:id]
@book.destroy
set_page_and_extract_portion_from Book.all
end
end
The index
method will use Geared pagination’s method to extract a selection from all the Book records. The destroy
method is going to destroy the record, ad you would expect, and then it’s going to call the pagination method again. This will bring up the records we’ll use in the Turbo Stream to replace the table.
The Views
Since there are two actions in the controller, we’ll use two corresponding erb
files. First, here is a partial that will be shared between the two actions, _book_records.html.erb
:
<% unless page.first? %>
<%= link_to "Prev", books_path(page: page.number - 1 ) %>
<% end %>
<span>
Showing page <%= page.number %> of <%= page.recordset.page_count %>
(<%= page.recordset.records_count %> total)
</span>
<% unless page.last? %>
<%= link_to "Next", books_path(page: page.next_param) %>
<% end %>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Publisher</th>
<th>Category</th>
<th></th>
</tr>
</thead>
<tbody>
<% page.records.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.author %></td>
<td><%= book.publisher %></td>
<td><%= book.category %></td>
<td><%= link_to "Delete", book_path(book, page: page.number), data: { 'turbo-method': :delete, 'turbo-confirm': "Are you sure?" } %></td>
</tr>
<% end %>
</tbody>
</table>
The first part of this partial is the pagination code that moves the page back and forth. The recordset count will show how many records are left, so we can confirm that a Book was deleted successfully. The Delete
link will include the current page in the request so the paginate method will load the correct set of records. It also uses the turbo-method
and the turbo-confirm
data attributes, which are different from the traditional Rails UJS data annotations. They behave the same, but will be used by Turbo instead of Rails UJS.
The index.html.erb
will have a turbo_frame_tag
so that Turbo knows which section to replace. The action
is supposed to update the page’s URL so that when the page reloads, it shows the proper page of records.
<h1 class="title">Paginate Through Books</h1>
<%= turbo_frame_tag "books", action: "advance" do %>
<%= render partial: 'book_records', locals: {page: @page} %>
<% end %>
The destroy.turbo_stream.erb
will use the replace command to reload the page of records. There is no need to remove the destroyed Book, because it is no longer in the database and won’t appear.
<%= turbo_stream.replace "books" do %>
<%= turbo_frame_tag "books", action: "advance" do %>
<%= render partial: 'book_records', locals: {page: @page} %>
<% end %>
<% end %>
Notice how both actions use the book_records
partial and pass the @page
variable as a local parameter.
Amazing Interactivity
One of the great things about HOTWire is how very little changes in the controllers and views. By rethinking how the destroy method is used, we can get a very interactive app without having to build any front end JavaScript.