50
Remotely loading tabbed content with Ruby on Rails and Hotwire
In today's example, we're building a interface that remotely retrieves a portion of the page content from an endpoint and replaces a targeted portion of the DOM with the response from the endpoint. We'll build this without writing any JavaScript and with only minor additions to the standard Rails code you already know how to write.
Here is what it will look like when we are finished. We won't be focused on styling today but our "tabs" will be fully functional and ready for you to add a nice looking Bootstrap or Tailwind skin.
To accomplish this, we will start with a new Rails 6.1 application, install Hotwire in the application, and then walk through the basics of adding Turbo Drive to our views.
I'm writing this assuming that you are comfortable with the basics of Ruby on Rails development and that you've never used Hotwire before.
You can find the complete source code for this tutorial on Github.
Let's dive in.
To get started, we're going to create a new Rails application and create the resources we need to start implementing our remotely fetched tabs. The application will be centered around a Person. A Person
can have Awards
and Credits
and those awards and credits are what we'll display in our tabs.
Let's get started with the following commands in our terminal:
rails new hotwire_tabbing -T
cd hotwire_tabbing
rails g scaffold Person name:string
rails g model Credit name:string person:references
rails g model Award name:string person:references
rails db:migrate
rails s
Visit http://localhost:3000/people in your browser and create a person to use for testing and update the Person model to finish up the relationships with Awards and Credits.
class Person < ApplicationRecord
has_many :awards
has_many :credits
end
Finally, in the Rails console (rails c
in your terminal), create a couple of Awards and Credits with the Person you created in the UI. We won't be covering creation of these resources in this lesson.
Person.first.awards << Award.create(name: "Best Director")
Person.first.credits << Credit.create(name: "Star Wars - The good one")
Now our application shell is all set up.
Next up, we'll install Hotwire and then we'll start building.
Add Hotwire to your Rails application by adding hotwire-rails
to your Gemfile and then running bundle install
from your terminal. If you prefer, bundle add hotwire-rails
from your terminal works too.
Once Hotwire is added to your Gemfile, install Hotwire by running rails hotwire:install
from your terminal. Hotwire is now installed and you're ready to start building.
Before moving on, be sure to restart your Rails server or you'll encounter some undefined method errors as you work through the rest of this guide.
Now that Hotwire is installed, our next step is to add controllers and views for Awards and Credits. These controllers and views will mostly look like standard Rails, with a few small adjustments to take advantage of the tools Hotwire provides.
First generate the controllers in your terminal:
rails g controller Awards
rails g controller Credits
And update the routes file to nest Awards and Credits endpoints under Person.
Rails.application.routes.draw do
resources :people do
resources :awards, only: %i[index]
resources :credits, only: %i[index]
end
end
Next, let's fill in our Awards controller:
class AwardsController < ApplicationController
before_action :set_person
def index
respond_to do |format|
format.html { render partial: 'awards/list', locals: { awards: @person.awards, person: @person }}
end
end
private
def set_person
@person = Person.find(params[:person_id])
end
end
Here we're using the person_id URL parameter to select the appropriate Person from the database, retrieiving that person's Awards, and rendering a list partial that doesn't yet exist.
Note that while we've chosen to use a partial here since that feels more comfortable for me, this guide would work fine using a regular old index view instead of a partial.
Since our list partial doesn't exist yet, go ahead and create it now:
touch app/views/awards/_list.html.erb
And then fill it in:
<%= turbo_frame_tag "details_tab" do %>
<div>
<%= render partial: "shared/tabs" %>
<div>
<h3>Awards won by <%= person.name %></h3>
<ul>
<% awards.each do |award| %>
<li><%= award.name %></li>
<% end %>
</ul>
</div>
</div>
<% end %>
Okay - now we've got a little bit of Hotwire code to review.
Our entire list partial is wrapped in a turbo_frame_tag
which will turn into a <turbo-frame>
HTML element when this view renders. The turbo frame helper provided by hotwire-rails requires an id argument ("details_tab" in our case) which Turbo uses to make DOM updates as needed.
Besides the turbo_frame helper, the rest of this view is standard Rails.
Note that the list partial relies on a shared/tabs partial that doesn't exist yet. Create that now with:
mkdir app/views/shared
touch app/views/shared/_tabs.html.erb
The tabs partial will render our tabs like this:
<div>
<%= link_to "Awards", person_awards_path(@person) %><br />
<%= link_to "Credits", person_credits_path(@person) %>
</div>
These tabs are just simple HTML and we'll render them in a couple of other places.
With the Awards controller and view built out, let's do the same thing for Credits before we finish up by updating the Person show page to tie everything together.
First create the credits list partial:
touch app/views/credits/_list.html.erb
And fill in the credits list partial with HTML that will look pretty familiar:
<%= turbo_frame_tag "details_tab" do %>
<div>
<%= render partial: "shared/tabs" %>
<div>
<h3>Credits for <%= person.name %></h3>
<ul>
<% credits.each do |credit| %>
<li><%= credit.name %></li>
<% end %>
</ul>
</div>
</div>
<% end %>
Then update the Credits controller:
class CreditsController < ApplicationController
before_action :set_person
def index
respond_to do |format|
format.html { render partial: 'credits/list', locals: { credits: @person.credits, person: @person }}
end
end
private
def set_person
@person = Person.find(params[:person_id])
end
end
Perfect.
Our last step is to update our Person show view to display the content of these tabs. Almost finished.
The Person show page will be responsible for displaying the tabbed content that we built out through the Awards and Credits resources in the last section. Update app/views/people/show.html.erb
like this:
<div>
<h2><%= @person.name %></h2>
<div>
<%= render partial: "awards/list", locals: { person: @person, awards: @person.awards } %>
</div>
</div>
Notice that here we don't have a single line of nonstandard Rails code here.
The Show view simply renders the Person's name and the awards list partial. The partial renders the tab navigation and the list of awards. All the heavy lifting is done behind the scenes by hotwire-rails by hooking into the details_tab <turbo-frame>
that our list partials render.
In your browser, head to a show page for a Person with awards and credits and see that clicking the navigation links toggles the list content between Awards and Credits without requiring a full page turn.
Great work!
This simple technique for remotely loading tabbed content with Hotwire reveals a powerful way of improving application performance and end user experience with minimal additional engineering overhead.
In a real application, moving requests for tabbed content that we don't need right away into a separate request and eliminating the need for a full page turn reduces server load and speeds up page load times without major architecture changes. Small teams and solo developers can use Rails + Hotwire to provide modern, highly-performant web applications quickly and without getting into the heavy world of single page applications and heavy JavaScript frameworks.
Further reading:
- For those new to Hotwire, the Turbo handbook is a great place to start
- The other side of the Hotwire package is Stimulus, a modest JavaScript framework designed to play nicely with Turbo as you build client-side interactivity and more complex user interfaces with Hotwire. Check out the Stimulus handbook here. Stimulus is an incredibly powerful tool and is worth learning for any developer interested in adding client-side interactivity quickly.
- If you're working with Rails, the source code for turbo-rails is a great reference. We're still in the early days of Turbo and you'll discover neat options in the source that might not be well-documented in guides like this one yet
I'm David - a solo founder and consultant.
I write about software engineering, Ruby on Rails and the Rails-y ecosystem, product management, and the lessons I learn as I work to build sustainable SaaS businesses.
My current project is Vestimonials, an async video communication tool to help companies collect and share video testimonials from their employees and customers. I'm available for Rails, product management, and strategy consulting.
Learn more about me or get in touch with me here.
50