One great aspect of Turbo frames is lazy loading. You can use this behavior to quickly load in the shell of a UI, and then lazy load all the data. Adding an animating loading status will help give the feeling of immediacy and “a lot of work is happening in the background” without the frustration that the page is stuck. We don’t even need any extra Javascript, since we can animate the page with CSS.
Loading many books
This tutorial will start from a previous example, Modeling More Complex Datasets, which used a Book model. We’ll start from scratch with a fresh Rails app and use TailwindCSS for the style.
$ rails new BookData -c tailwind
$ cd BookData
$ rails g model Book title:string author:string publisher:string category:string isbn:string dewey_decimal_number:string binding:integer
$ rails db:migrate
We’re going to make binding an enum since there are only a few options, so change models/book.rb
to this:
class Book < ApplicationRecord
enum binding: [:hardcover, :paperback]
end
To test our system with plenty of books, We will use the Faker gem to create about 2000 fake books.
In our Gemfile:
gem 'faker'
Then run
$ bundle install
Then we’ll create the books in our db/seeds.rb
file.
2_000.times do |i|
Book.create!( title: Faker::Book.title,
author: Faker::Book.author,
publisher: Faker::Book.publisher,
category: Faker::Book.genre,
isbn: "#{Faker::Number.number(digits: 3)}-#{Faker::Number.number(digits: 1)}-#{Faker::Number.number(digits: 2)}-#{Faker::Number.number(digits: 6)}-#{Faker::Number.number(digits: 1)}",
dewey_decimal_number: "#{Faker::Number.number(digits: 3)}.#{Faker::Number.number(digits: 3)}",
binding: Faker::Number.between(from: 0, to: 1))
print '.' if i % 100 == 0
end
Now we have a lot of books records that we can use.
Let’s build an index page so that we can look at our books.
$ rails g controller books index
This will update routes.rb
:
Rails.application.routes.draw do
get 'books/index'
end
And we need to update app/controllers/books_controller.rb
file with an index action, and we’ll start by loading all the books.
class BooksController < ApplicationController
def index
@books = Book.all
end
end
And finally, a view at app/views/books/index.html.erb
.
<div>
<h1 class="font-bold text-4xl">All Books</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Publisher</th>
<th>Category</th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.author %></td>
<td><%= book.publisher %></td>
<td><%= book.category %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
Run ./bin/dev
and visit it in the browser.
Lazy Loading
The URL path /books/index
is a little awkward. It would be great if it would load when the site loads up. Let’s add a root controller that will load the books page:
$ rails g controller root index
Change the routes.rb
file to load at the root page:
Rails.application.routes.draw do
get 'books/index'
root "root#index"
end
Add a turbo-frame
element to the root/index.html.erb
page:
<div>
<h1 class="font-bold text-4xl">Root Page</h1>
<%= turbo_frame_tag "books", src: books_index_path %>
</div>
If you don’t change the book’s index page, this will load all the books in, and overwrite the URL. We want to hide the /books/index
path, so let’s wrap the table in views/books/index.html.erb
in a turbo_frame_tag
:
<div>
<h1 class="font-bold text-4xl">All Books</h1>
<%= turbo_frame_tag "books" do %>
<table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Publisher</th>
<th>Category</th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.author %></td>
<td><%= book.publisher %></td>
<td><%= book.category %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>
Now, when you load the root page, there is a skeleton that loads in the books table.
Animate the loading
Loading all these books could take a while. This could stand in for a different page with more complicated database calls that could take a few seconds. Let’s put in a small skeleton, animate a shimmering effect, and give the appearance that the page is working in the background. Look at the revised /views/root/index.html.erb
:
<div>
<h1 class="font-bold text-4xl">Root Page</h1>
<%= turbo_frame_tag "books", src: books_index_path do %>
<table class="animate-pulse">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Publisher</th>
<th>Category</th>
</tr>
</thead>
<tbody>
<% (1..10).each do %>
<tr>
<td><div class="bg-slate-200 h-8 rounded w-64"></div></td>
<td><div class="bg-slate-200 h-8 rounded w-64"></div></td>
<td><div class="bg-slate-200 h-8 rounded w-64"></div></td>
<td><div class="bg-slate-200 h-8 rounded w-64"></div></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>
The turbo-frame now has a block that puts in a skeleton table. It uses the tailwind animate-pulse class, which is a pulsing CSS animation. Instead of iterating through book records, it generates 10 rows. Each td
cell has a rounded div that acts a visual placeholder. Loading the page shows a pleasant placeholder while all the books come in over the wire.
Free Interactivity
This shows how we can use a lazy loading page to add interactivity to our page with little extra work, and no extra JavaScript. You could imagine having to see up a Stimulus controller that loaded data in when it connected. Turbo allows us to skip even that step, and just use basic HTML and an extra Rails controller. You could even build a dashboard that loaded data from several sources.
One comment on “Adding Loading Screen with Turbo”
[…] You can probably come up with other processes that need to display background progress, such sending out batches of notifications, but let’s work through processing books like in our previous Book example. […]