Turbo Frames on Rails
08 Oct 2021Turbo, part of the Hotwire tripartite, gives you the tools to write dramatically less custom JavaScript than you would otherwise need to build modern, performant web applications.
Turbo is composed of Turbo Drive
, Turbo Frames
, Turbo Streams
, and Turbo Native
. Each is a valuable piece of the puzzle but today we’re going to focus on Turbo Frames
.
Turbo Frames “allow predefined parts of a page to be updated on request.” Used wisely, frames allow developers to decompose their UI into independently updated pieces, quickly.
Why should I use Turbo Frames?
Turbo Frames unlock a huge amount of potential with minimal changes to existing code and they can be gradually introduced to existing projects without necessitating major architectural changes.
In greenfield projects designed with Turbo Frames in mind, a small team of developers can use frames to deliver fast, efficient user interfaces in dramatically less time than would be required when build a SPA-powered front end.
While Turbo Frames can be used with many different tech stacks, Rails developers will find the tight integration of frames into Rails (via the turbo-rails gem) makes using frames in Rails a breeze.
The Turbo documentation gap
As with many new technologies, one of the barriers to getting started with Turbo Frames is inconsistent/incomplete documentation. Along with problems in the official documentation, much of the tutorial content written about Turbo Frames is already out of date.
Turbo has evolved quickly since its release in December of 2020, and the documentation and supporting tutorials from content creators have struggled to keep pace.
As an example of the documentation gap, turbo-rails
doesn’t have any dedicated usage documentation, instead just linking to the base Turbo docs and letting users figure out the rest.
This means that learning to use Turbo Frames in your Rails application today often requires reading the docs, then digging through source code, and then Googling your way through Github issues and forum posts. Turbo Frames can be difficult to approach.
In time the documentation will improve and the community will coalesce around best practices and standards that can be more easily communicated to new users.
In the meantime, here we are.
What is this article? Who is it for?
Today, I’m going to share what I’ve learned over nearly a year building applications with Turbo Frames, from small hobby projects and experiments to multiple large, legacy production applications.
By doing so, I’m hoping to address the gaps in documentation, to save you from spending too much time reading source code, trawling forum posts and Github issues.
I will cover content you can find in the documentation (if you know where to look), gotchas and tricks learned from real world experience and picked up from reading way too much about Turbo and Turbo Frames since Turbo’s initial release. I’ll also share patterns that illuminate some of the common web application needs that frames are perfect for.
This content is geared towards Rails developers interested in using Turbo. The code samples and concepts will be illustrated using Ruby and Rails code and conventions. If you aren’t working in Rails, you should still find plenty of value here, but know that you may not have access to convenience methods like <%= turbo_frame_tag %>
in your chosen language and framework.
Let’s dive in!
What are Turbo Frames?
At a high level, Turbo Frames are pieces of a webpage that can be updated independently, without impacting the rest of the content on the page.
Links and forms within a frame will, by default, attempt to update only the content of the containing frame, whether the server sends a completely new HTML document or only a page fragment.
Turbo Frames allow developers to decompose a page into pieces of content that can be updated individually as new information is received from a server.
In practice, common use cases for Turbo Frames include:
- Tabbed content
- In-line editing
- Searching, sorting, and filtering data
Let’s start looking at code.
Constructing a frame
A basic Turbo Frame, rendered with the built-in helper from Turbo Rails (and using erb), looks like this:
The only required argument for turbo_frame_tag
is an id.
When the helper is processed, the final HTML is:
Instead of providing a static id, we can also pass in an active record object, which the helper will use to generate a unique id via dom_id(object.id)
.
Frames can also receive an src
attribute. When src
is supplied, the frame will be populated after the initial page load via a separate HTTP request to the frame’s src
:
Here, the page will initially load the turbo frame with the placeholder div and then immediately a request to comments_path
endpoint is made and the content of the Turbo Frame is replaced with the response from the server, provided our server returns HTML with a matching Turbo Frame tag.
What’s a matching Turbo Frame tag? Let’s look at that next.
Responding with frame content
The power of Turbo Frames is in replacing pieces of the page with content sent from the server — without touching the rest of the DOM.
Updating an existing frame just requires responding to a request with HTML that contains a Turbo Frame element with an id that matches the id of the frame target sent in the request.
Taking the comments
example from above, when a request is made to /comments
, the server should respond with HTML that contains a frame like this:
The content inside of the matched frame is replaced with the updated content from the server, without touching the rest of the page.
Navigating within frames
By default, links and forms within a Turbo Frame will perform the navigation within the frame, rather than performing a full page turn.
For a simple example, we can imagine that we have a list of comments on a page and that each comment is wrapped in a Turbo Frame tag, like this:
This ERB results in rendered HTML:
With this Turbo-powered HTML in place, clicks on the edit links will make a Turbo Frame request to /comments/:id/edit
, retrieve some HTML, and return that HTML to the browser.
The Turbo Frame request is a normal HTML request with an additional Turbo-Frame
header included in the request, with a value that matches the id of the target Turbo Frame.
In turbo-rails
, the presence of this header can be used to identify a Turbo Frame request and respond with appropriate content, like this:
When turbo-rails
responds to a Turbo Frame request, it automatically removes the layout from the HTML response. Since Turbo will discard all of the HTML but the requested frame, Rails skips rendering content that it knows won’t be used.
When a response to a Turbo Frame request is received, Turbo Drive replaces the content of the target frame, leaving the rest of the page untouched.
Continuing with the above comments example, the edit view may look something like this:
After clicking to edit a comment then, the updated HTML after Turbo processes the response would look something like this:
Finishing up this example, what happens when the user submits the comment form?
After submit in a normal Rails action, we might redirect the user to the comment show page. When the request occurs within a Turbo Frame, a “redirect” still occurs, but rather than redirecting the page in the browser, the redirect is followed, the new HTML content is sent from the server, and that HTML is used to update the frame.
In our example, the comment#update
controller action might look like this:
And the show view:
This behavior allows us to build Turbo Frame-powered interfaces while making very few or no changes to a standard Rails controller, making it possible to prototype in Rails very quickly.
With just what we’ve learned so far, we’re already in position to start building simple interfaces with Turbo Frames that allow us to add significant amounts of interactivity to our application with very few changes to the Ruby and HTML we’d have in any Rails app.
Let’s keep exploring, there’s plenty more to see.
Breaking out of a frame
While navigating within a frame is very handy, sometimes, a form or link within a frame needs to break out of the frame and perform a normal page turn.
To add that behavior to a navigation element within a frame, simply add data-turbo-frame="_top"
to the element, like this:
Targeting a frame from the outside
Eventually, you’ll find yourself needing to update the content of a frame using a link that isn’t wrapped in your target frame. This often comes up when building tabbed content; the navigation menu will be placed outside of the Turbo Frame that contains the actual content.
To handle situations like this, we can use the same turbo-frame
data attribute to tell Turbo that the navigation should occur in a specific turbo frame.
Using our tabbed content example, we may have a page layout like this:
Clicks on either of the user
links will generate a request with a Turbo-Frame: main
header and the response will update the content of the main
turbo frame, even though the links are not wrapped within the frame.
Lazy loading Frames
The src
attribute on a frame can be combined with loading=lazy
to mark a frame as lazy-loaded. Lazy-loaded frames will not fetch their content from the server until they’re visible on the page.
Lazy loading is especially helpful for modals, popovers, and other non-critical, below-the-fold content.
Lazy loaded frames can be combined with a spinner or other loading indicator, like this:
Note that loading="lazy"
must be combined with a src
attribute, otherwise lazy loading does nothing
Frame events
Turbo comes packed with lifecycle events, including two frame specific events:
- turbo:frame-render
- turbo:frame-load
These events (added in Turbo 7.0.0-rc2) fire each time a frame is loaded or reloaded. Developers can use these events to attach behavior to an element within a frame or animate the entry of a frame element, and they pair nicely with Stimulus.
In addition to these built in events, a useful complement to the emitted events is the busy
attribute. The busy attribute is automatically applied to a frame when a request for the frame’s content begins and is removed when the request finishes.
This attribute can be useful for adding loading indicators to long running requests and otherwise tracking the state of frames between the beginning of a frame request and the turbo:frame-render
and turbo:frame-load
events firing.
Refreshing Frames
Sometimes, users need the ability to refresh the contents of a frame without refreshing the entire page.
In ideal circumstances, a Turbo Stream broadcast automatically updates the contents of the frame without needing a manual refresh, we don’t always get to work in ideal circumstances.
In earlier versions of Turbo, refreshing a frame required workarounds like appending a timestamp to the src
attribute. Today, those workarounds are no longer necessary and Turbo will happily update a frame any number of times with an indentical src
on each request.
We can build a refreshable frame like this:
Here, the first time a user clicks the update link, the turbo-frame
will be updated with an src
like this:
Additional clicks on the update link will fetch updated scores from the server and reload the content of the frame.
Note that a currently open PR (as of the time this article was last updated, December 2021) proposes an alternative implementation of reloadable frames that will prevent the reloadable
attribute from being exposed while keeping the behavior intact. In future versions of Turbo the reloadable
attribute may not longer exist or be neccessary.
Conditional template rendering using variants
As you use Turbo Frames in Rails, you’ll eventually run into the desire to have a single controller action respond to both Turbo Frame requests and regular, full-page requests.
A common example of this is rendering a /new
page as both a standalone page and as frame content inside of a modal. You could implement a conditional in your controller action to check for the Turbo Frame header, but that quickly starts to clutter up your controllers.
An alternative approach is to use variants to render different content based on the inbound request headers, to keep your controllers cleaner.
This approach adds a before_action to the ApplicationController that checks if the inbound request is a Turbo Frame request using the turbo_frame_request?
method provided by turbo-rails. When the request is a turbo frame, Rails will look for a Turbo Frame variant (new.html+turbo_frame.erb
, for example) and render that variant when it exists. Otherwise, the default html.erb
view will be used
This approach was originally described here. While there was a proposal to add this variant-based approach to turbo-rails, those proposals were rejected. The guidance from the maintainers is that the turbo_frame_request?
to detect Turbo Frame requests is the only support that will be added because “If you’re branching all your responses for frames vs not, something isn’t right, and we should investigate those pressures in a different way.”.
In my experience, judicious use of variants to serve different content for Frame requests is sometimes the simplest and cleanest path to achieve a desired outcome but YMMV.
Limitations & Gotchas
Turbo Frames and page layouts
Turbo Frames in the DOM are implemented as custom HTML elements (the <turbo-frame>
tag we’ve seen in this article). We can add classes to them (<%= turbo_frame_tag post, class: "classes" %>
) and style them; however, because they exist in the DOM and need to wrap their content to be useful, inserting them into existing layouts can cause issues without some planning.
In particular, you can’t place a frame inside of a table row. While there are workarounds for this, it is worth knowing that tables and Turbo Frames don’t play well together out of the box.
In addition to table issues, you may sometimes run into layout issues with flex box and grid layouts when frames are the direct descendant of the container element. You’ll sometimes find that your nicely spaced three element flex layout now breaks because all three elements need to be wrapped in a single <turbo-frame>
tag.
A route worth exploring in these situations is adding display: contents
to the <turbo-frame>
element, as mentioned here.
Responding to a Turbo Frame request without a matching Turbo Frame
As you build applications with Turbo Frames, you will inevitably run into an issue in development where you click a link within a frame and all of the content within the frame disappears. You’ll check the Rails logs and see the view you expected was rendered without errors.
Why did everything disappear?
The first thing to do when this happens is to check the JavaScript console for errors. Turbo’s JavaScript expects that the HTML response to a Turbo Frame request will contain a matching Turbo Frame element. If no match is found, Turbo will empty the frame of all content and raise an error like this in the JavaScript console:
This can be a little confusing at first, since the content renders fine from the server but once you get used to Turbo processing the rendered HTML and updating the DOM, you’ll be able to quickly debug missing frame element errors like this one.
When in doubt, check for JavaScript errors.
Updating page URL when navigating within frame
When a user clicks on a link within a frame, the URL of the page won’t change. This makes sense most of the time — the user hasn’t navigated to a new page, they’ve simply updated some of the content on the existing page.
One use case where the URL not changing can be problematic is when the user is applying sorting and filtering options by clicking links within a frame. Imagine a table layout with links to sort the table by each column, for example.
In those cases, a developer may wish to update the URL of the page to reflect the currently applied filters so that the user can copy/paste the URL to share a specific view of the table.
As of Turbo’s 7.1 release, developers can now update the page URL from a frame navigation by adding a turbo-action
data attribute to the form or link that triggers the frame navigation. Options for turbo-action
are replace
, to replace the current URL in the browser’s history, or advance
, to push a new entry in the history. When building search forms, you’ll likely want to use advance
to update the page URL with each new search. In Rails, using a standard form_with
search form, your code to push frame navigation into the browser’s history might look like this:
Wrapping up
Today we looked in detail at the current state of Turbo Frames — what they are, how to use them in Rails, and the some of the ways frames can help you build modern, efficient applications.
Turbo Frames in Rails applications naturally pair very well with Stimulus and work exceptionally well when paired with Turbo Streams to manage broadcasted updates and more targeted content updates.
With Hotwire coming by default to new Rails 7 applications, I’m happy to report that after a year of use, building Ruby on Rails applications with the Hotwire stack is simple, straightforward, and adds very little overhead to your development experience. For many Rails applications, all of the reactivity you need can be achieved with the Hotwire stack, and Turbo Frames will play a key role in many applications in the years to come.
While you begin to experiment, keep in mind that Turbo is still in very active development. Throughout this article, I linked to open PRs that will improve Turbo Frames and make some of the techniques described in this article simpler to implement — as time goes on, expect Turbo and Turbo Frames to continue to improve and for turbo-rails
to continue to add functionality to help Rails developers use frames effectively in their applications.
Ready to go deeper? (Some shameless self-promotion ahead, sorry!)
- I wrote about using the Hotwire stack to build server rendered modal forms in Rails
- Build a typeahead search interface with Turbo Frames
- Use a Turbo Stream + Turbo Frame combo to render different content to different users from a single stream broadcast
- Read my exploration of Turbo Streams
Finally, if you want to learn about implementing Turbo Frames in a complex Rails application, I wrote a book on building a modern Ruby on Rails application with my preferred stack, including Hotwire, StimulusReflex, CableReady, and friends. The book is written in this same, step-by-step tutorial style that you find throughout this blog and aims to offer solutions for a variety of common, real-world web application problems without reaching for the weight of a full frontend framework.
That’s all for today. As always, thanks for reading!