Breaking out of Turbo Frames

09 Jan 2022

By default when navigating using an achor element nested inside a <turbo-frame> element, assuming the response contains a matching <turbo-frame> only that frame will be replaced, rather than the entire page. This is how Hotwire adds Single Page Application (SPA) functionality to the conventional Rails stack.

<%= turbo_frame_tag :week do %>
  <%= link_to 'Previous Week', previous_week_path %>
  <%= link_to 'Next Week', next_week_path %>

  <%= link_to 'View Workplace', workplace_path(@workplace) %>

  <div>...content for the selected week</div>
<% end %>

In the above example, the matching <turbo-frame id="week"> element in the response from clicking any of the links will be swapped for the frame currently on the page. This is exactly what we want for the first two links but for the third link (View Workplace) we actually do want to navigate to a new page which isn't going to happen. So how do we solve this?

data-turbo-frame="_top"

In order to "break out" of the <turbo-frame> that is currently wrapping our 'View Workplace' link we can add the data-turbo-frame="_top" data attribute to our link. This will notify Turbo that we really do want to navigate to a new page when we click this link, not just replace the current Turbo Frame context. In rails it looks like this:

<%= turbo_frame_tag :week do %>
  <%= link_to 'Previous Week', previous_week_path %>
  <%= link_to 'Next Week', next_week_path %>

  <%= link_to 'View Workplace', workplace_path(@workplace), data: { turbo_frame: '_top' } %>

  <div>...content for the selected week</div>
<% end %>

This solves our problem perfectly but there is also another way that is useful to know about.

target="top" & data-turbo-frame="self"

This looks pretty similar on the face of it but actually does the reverse of the above solution. By adding target="_top" to our encapsulating <turbo-frame id="week"> so it looks like <turbo-frame id="week target="_top"> we can make it so that the default behaviour of clicking a link inside the context of a Turbo Frame is to navigate to a new page.

<%= turbo_frame_tag :week, target: "_top" do %>
  <%= link_to 'Previous Week', previous_week_path %>
  <%= link_to 'Next Week', next_week_path %>

  <%= link_to 'View Workplace', workplace_path(@workplace) %>

  <div>...content for the selected week</div>
<% end %>

Wonderful! But this means our 'Previous Week' & 'Next Week' links are also going to navigate to a new page, which we don't want. But fear not, this can be resolved by using data-turbo-frame="_self" and would look like this:

<%= turbo_frame_tag :week, target: "_top" do %>
  <%= link_to 'Previous Week', previous_week_path, data: { turbo_frame: '_self' } %>
  <%= link_to 'Next Week', next_week_path, data: { turbo_frame: '_self' } %>

  <%= link_to 'View Workplace', workplace_path(@workplace) %>

  <div>...content for the selected week</div>
<% end %>

By using this we are asking Turbo politely to only replace the encapsulating <turbo-frame> when we click these links while still navigating to a new page when we click the View Workplace link.

Conclusion

Both of the above solutions solve the same problem in a different way. It's up to you to decide which is the most suitable for each Turbo Frame you use but in general I use the following rule

If the majority of the links within a Turbo Frame navigate to a new page use target="_top" while if the majority are replacing the encapsulating Turbo Frame use data-turbo-frame="_top" on links that you wish to break out of the frame.

Hopefully this rule of thumb will help you make the decision that best suits your use case. Happy Hotwiring!