DEV Community

Cover image for How to paginate items using Turbo
Nick Pezza
Nick Pezza

Posted on

How to paginate items using Turbo

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
Enter fullscreen mode Exit fullscreen mode
<%# 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>
Enter fullscreen mode Exit fullscreen mode
<%# 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 %>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

If we reload our page and try it out, it works!
Screen Recording 2020-12-22 at 4.43.06 PM

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>
Enter fullscreen mode Exit fullscreen mode

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.)

Top comments (6)

Collapse
 
ben profile image
Ben Halpern

Forem uses a lot of similar techniques as Hotwire and Turbo. Inspired by other ideas passed around by Basecamp, but we wound up building in our own approaches. I wonder if there would ever be a day where we shift towards adopting more of this.

We already use some Stimulus, but it's kind of all-or-nothing in terms of really doing this the right way.

Collapse
 
iamsohair profile image
Sohair Ahmad

I wonder if this is working without format.turbo_stream under respond_to block, or maybe I am missing some knowledge on this?

Collapse
 
pezza profile image
Nick Pezza

You don't need the turbo_stream format because you are clicking links inside a turbo_frame and turbo knows when that happens to looks for a matching id on the requested html page.

turbo.hotwired.dev/reference/frame...

Collapse
 
justinmcodes profile image
jmarsh24

It would be great to have a follow up with this article on how to integrate it with turbo stream to target on the paginated records from the new page.

Collapse
 
matiascarpintini profile image
Matias Carpintini

Looks good! But this break broadcasts :/

Collapse
 
pezza profile image
Nick Pezza

How do you mean?