Nick Pezza
Posted on December 22, 2020
Basecamp recently released Hotwire which includes Turbo. Using Turbo, we can quickly paginate a long list of items that can be asynchronously loaded without any javascript.
Let's say we have an app that lists credit card transactions. This list can become very long which we wouldn't want to load upfront due to the performance impact. To avoid loading all the transactions upon page load but, still allow our users to see all transactions, we will only render the first few transactions initially and then show a "Load More" link. The "Load More" link will grab more transactions, append them to the existing list of transactions, and then update our "Load More" link to point to a new URL for the next collection of transactions.
(Note: We are going to use Geared Pagination for pagination but this solution will work with any pagination solution so long as you know the current and next page numbers.)
Here's where our app currently stands:
# app/views/transactions_controller.rb
class TransactionsController < ApplicationController
def index
set_page_and_extract_portion_from(Transaction.all)
respond_to do |format|
format.html
format.js
end
end
end
<%# app/views/transactions/index.html.erb %>
<p id="notice"><%= notice %></p>
<div>
<%= link_to 'New Transaction', new_transaction_path %>
</div>
<h1>Transactions</h1>
<div>
<ul><%= render @page.records %></ul>
<% unless @page.last? %>
<%= link_to "Load More", transactions_path(page: @page.next_param), remote: true, id: "load-more" %>
<% end %>
</div>
<%# app/views/transactions/index.js.erb %>
document.querySelector("ul").insertAdjacentHTML("beforeend", "<%= j render partial: @page.records, as: :transaction %>");
<% if @page.last? %>
document.querySelector("#load-more").remove();
<% else %>
document.querySelector("#load-more").href = "<%= transactions_path(page: @page.next_param) %>";
<% end %>
This solution works fine. Code is pretty concise and explicit, but we are using a separate js.erb view which we now also have to maintain along with the html.erb view for the index action.
Using Turbo we can remove app/views/transactions/index.js.erb
and the respond_to
block in the controller since we will only be responding to HTML.
In app/views/transactions/index.html.erb
we will make the following changes inside the ul
element:
<ul>
<%= turbo_frame_tag "transactions-#{@page.number}" do %>
<%= render @page.records %>
<%= turbo_frame_tag "transactions-#{@page.next_param}" do %>
<% unless @page.last? %>
<%= link_to "Load More", transactions_path(page: @page.next_param) %>
<% end %>
<% end %>
<% end %>
</ul>
If we reload our page and try it out, it works!
The trick to making this work is nesting the turbo-frames. On this first page, our outer frame has the id of transactions-1
and the inner one has the id of transactions-2
. When we click the "Load More" link the server is going to respond with the inner HTML of the body
element of what page 2 looks like. In that case, our outer frame has the id of transactions-2
and our inner one has the id of transactions-3
. Once we get that response, Turbo will replace any frames that occur on the initial page and on the one sent back.
Since the transactions-1
turbo-frame
doesn't appear on the second page, nothing happens to it. But there is a transactions-2
turbo-frame
on both our first and second pages so that frame is replaced. That new frame will render the Transaction
's on the second page right below the first page's transactions, and remove the first page's "Load More" and replace it with a "Load More" link to the third page. Our HTML would end up looking something like:
<ul>
<%= turbo_frame_tag "transactions-1" do %>
<%= render @page.records %> <%# page 1 records %>
<%= turbo_frame_tag "transactions-2" do %>
<%= render @page.records %> <%# page 2 records %>
<%= turbo_frame_tag "transactions-3" do %>
<%= link_to "Load More", transactions_path(page: 3) %>
<% end %>
<% end %>
<% end %>
</ul>
Magic 🪄!
(Note: Styling could become an issue with this solution with elements being nested inside turbo-frame tags but so far I haven't run into any issues.)
Posted on December 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.