Remotely loading tabbed content with Ruby on Rails and Hotwire

Hotwire is a set of tools for building web applications by sending HTML instead of JSON over the wire. Hotwire gives us a framework for making DOM updates without writing much, or any, JavaScript while delivering fast, modern-feeling web applications.

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.

A screen recording of a user toggling between an Awards and Credits tab on a web page, with the content changing on each click

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.

Set up our project

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.

Install Hotwire

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.

Setup awards and credits

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.

Tie everything together

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.

A screen recording of a user toggling between an Awards and Credits tab on a web page, with the content changing on each click

Great work!

Wrapping up

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

Hotwiring Rails newsletter.

Enter your email to sign up for updates on the new version of Hotwiring Rails, coming in spring 2024, and a once-monthly newsletter from me on the latest in Rails, Hotwire, and other interesting things.

Powered by Buttondown.