How do I use turbo frames to create tab navigation?

I’m trying to wrap my head around using Hotwire with Rails.

I have simple tab navigation menu that is at the top of my page inside a <%= turbo_frame_tag 'account_navigation', target: 'tab_content' do %> with a Dashboard link, Calendar link, etc.

Underneath that I have a <%= turbo_frame_tag 'tab_content', src: dashboard_path %> that loads the dashboard.html.erb content initially.

What I would like is for when I click a tab, it replaces the tab_content turbo frame with the corresponding content, e.g. Clicking the calendar link should replace the tab_content with the turbo frame in calendar.html.erb.

Is there a way to do this without including the tab navigation code on every page that I want to use as tab_content ?

What if you create additional templates to wrap these existing ones, like: tab_calendar.html.erb, this file would contain the tab navigation related code, as well as render the original template, it would let the original calendar.html.erb untouched

So after a lot of video watching/pausing, I figured out a solution and its actually surprisingly easy.
If you are wanting to create a tab navigation like I did where the navigation menu is outside of the turbo frame so you aren’t repeating code over an over, you just need to add a data-turbo-frame="MyTurboFrame" attribute to your nav links. Here is an example using Rails.

# show.html.erb
# Nav menu/ not wrapped in a turbo_frame
<ul class="tabs">
  <li><%= link_to "Dashboard", dashboard_path(@user), class: "nav-link", data: {"turbo-frame": :"tab_content"} %></li>
  <li><%= link_to "Calendar", calendar_path(@user), class: "nav-link", data: {"turbo-frame": :"tab_content"} %></li>
</ul>

     # tab_content turbo frame that loads the dashboard initially
<%= turbo_frame_tag "tab_content", src: dashboard_path(@user) %>

dashboard.html.erb

<%= turbo_frame_tag "tab_content" do %>
  Dashboard code...
<% end %>

calendar.html.erb

<%= turbo_frame_tag "tab_content" do %>
  Calendar code...
<% end %>

users_controller.rb

def dashboard
  respond_to do |format|
    format.html
  end
end

def calendar
  respond_to do |format|
    format.html
  end
end

This allows the turbo_frame to be replaced with the frame on the corresponding dashboard and calendar pages. And also allows the page to be directly visitable if JS is not on for example. So visiting users/1/calendar will actually load the calendar page content on screen.

Hope this helps anyone looking to do something similar. If anyone sees a better way of doing things, let me know.

5 Likes

I thought you didn’t want you put the turbo_frame_tag “tab_content” on dashboard/calendar files

No, I didn’t want to repeat putting the tab navigation links on the dashboard/calendar files.
The turbo_frame_tag “tab_content” has to wrap the content of the dashboard/calendar files in order for turbo to replace the frame tag in my show.html.web

1 Like

That’s awesome! However, does anyone know we can keep track of which link is active based on a Turbo Frame?

For example clicking calendar would add a class to that link:

# Nav menu/ not wrapped in a turbo_frame
<ul class="tabs">
  <li><%= link_to "Dashboard", dashboard_path(@user), class: "nav-link", data: {"turbo-frame": :"tab_content"} %></li>
  <li><%= link_to "Calendar", calendar_path(@user), class: "nav-link active-link", data: {"turbo-frame": :"tab_content"} %></li>
</ul>

I went with a different approach for tabbed navigation than the OP but here’s I did, which includes tracking active pages by adding an active CSS class. The difference is the tabbed navigation ends up being within the frame.

This was the code necessary to wrap content in a frame, it would be expected to be placed in one of your layout files:

<%= turbo_frame_tag "tabbed_content", data: { "turbo-action": "advance" } do %>
  <%= render 'shared/tabbed_nav' %>

  <%= yield %>
<% end %>

That’s it. Now every time you click one of your links defined in shared/tabbed_nav, it along with whatever views you render will be updated in the frame without needing to use turbo frame tags anywhere else. All links stay contained in the frame and URL updating works.

In shared/tabbed_nav you could have:

<nav>
  <%= tabbed_menu_item "A", a_path %>
  <%= tabbed_menu_item "B", b_path %>
</nav>

And that helper is defined as:

def tabbed_menu_item(text, link)
  active_class =
    if request.path.include?(link)
      'active'
    else
      ''
    end

  link_to text, link, class: active_class
end
  • When you’re within any controller actions for A its tab link will have an active class while B’s does not
  • When you’re within any controller actions for B its tab link will have an active class while A’s does not
  • You can add more links and complexity as needed

I know the naming of things I provided aren’t super descriptive. I kept them generic for this reply but in a practical application everything is named based on what the tabs are for.