The Turbo Frames API requires that a request made from within
a turbo-frame must receive a response containing a corresponding
turbo-frame of the same id.
Because Rails encourages the reuse of partials and views, this can lead to situations where you need to conditionally render a Turbo Frame. One such example is inline editing, which we’ll explore in this tutorial.
Our Base
Our starting point does not yet warrant the need to conditionally render any of
the Turbo Frames because all three instances use the same HTML. Most notably
between the show and index views. This is because both of those views render
the _post partial.

# app/views/posts/index.html.erb
<% @posts.each do |post| %>
<%= turbo_frame_tag dom_id(post) do %>
<%= render post %>
<%= link_to "Edit", edit_post_path(post) %>
<% end %>
<% end %>
# app/views/posts/_post.html.erb
<div>
<p>
<strong>Title:</strong>
<%= post.title %>
</p>
<p>
<strong>Body:</strong>
<%= post.body %>
</p>
</div>
When we click the “Edit” link from the index view, we load the corresponding
turbo-frame from the edit view. When the form is submitted, the #update
action redirects to the show view, which also contains a corresponding
turbo-frame.
# app/views/edit.html.erb
<%= turbo_frame_tag dom_id(@post) do %>
<%= render "form", post: @post %>
<%= link_to "Cancel", :back %>
<% end %>
def update
if @post.update(post_params)
redirect_to post_url(@post), notice: "Post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
# app/views/posts/show.html.erb
<%= turbo_frame_tag dom_id(@post) do %>
<%= render @post %>
<%= link_to "Edit", edit_post_path(@post) %>
<% end %>
The key here is that the index and show views are both using the same
_post partial. This makes for a seamless experience. The only “gotcha” is that
this also means we’ve inadvertently enabled inline editing on the show page
too.

However, this is a contrived example and does not reflect a real-world design. It’s common to render content differently when viewed in different contexts.
Let’s explore that next.
The Problem
Let’s update our _post partial and show view so that we see a teaser of the
post on the index page, and the full post on the show page.
--- a/app/views/posts/_post.html.erb
+++ b/app/views/posts/_post.html.erb
@@ -1,12 +1,12 @@
-<div>
- <p>
- <strong>Title:</strong>
- <%= post.title %>
- </p>
+<article>
+ <h2><%= post.title %></h2>
<p>
- <strong>Body:</strong>
- <%= post.body %>
+ <%= post.body.truncate(20) %>
</p>
-</div>
+ <td>
+ <%= link_to "Edit", edit_post_path(post) %>
+ <%= link_to "Show this post", post, data: { turbo_frame: "_top" } %>
+ </td>
+</article>
--- a/app/views/posts/index.html.erb
+++ b/app/views/posts/index.html.erb
@@ -8,11 +8,7 @@
<% @posts.each do |post| %>
<%= turbo_frame_tag dom_id(post) do %>
<%= render post %>
- <%= link_to "Edit", edit_post_path(post) %>
<% end %>
- <p>
- <%= link_to "Show this post", post %>
- </p>
<% end %>
</div>
--- a/app/views/posts/show.html.erb
+++ b/app/views/posts/show.html.erb
@@ -1,7 +1,10 @@
<p style="color: green"><%= notice %></p>
<%= turbo_frame_tag dom_id(@post) do %>
- <%= render @post %>
+ <h1><%= @post.title %></h1>
+ <p>
+ <%= @post.body %>
+ </p>
<%= link_to "Edit", edit_post_path(@post) %>
<% end %>
Now that we’re no longer sharing the same markup between the index view and
the show view, we end up rendering the markup for the show view when we edit
a post from the index view. Instead of rendering a teaser, we render the
whole post.

However, this is not an issue when editing from the edit page, since we expect
to see the whole post after making an edit.

A Simple Solution
Here’s where we need introduce the concept of conditionally rendering a Turbo Frame.
What we want to do is render the simple _post partial when a request is made
from the index view. Otherwise, if the request is made from the edit view,
we want to render the show view.
Fortunately, this can be easily solved with redirect_back_or_to.
Redirects the browser to the page that issued the request (the referrer) if possible, otherwise redirects to the provided default fallback location.
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -33,7 +33,7 @@ class PostsController < ApplicationController
# PATCH/PUT /posts/1 or /posts/1.json
def update
if @post.update(post_params)
- redirect_to post_url(@post), notice: "Post was successfully updated."
+ redirect_back_or_to post_url(@post), notice: "Post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
In this case, when we edit a post from the index view, it will respond with
index which already has a turbo-frame rendering the _post partial. The
same concept applies for when editing a post from the edit view.

A More Complex Example
Our current implementation only works because there’s a turbo-frame on the
index, show and edit views. What if we didn’t have that luxury? For
example, what if we didn’t want to inline edit on the show page?
We can’t use redirect_back_or_to because we want to redirect to the show
view when making an edit on the edit view, but still maintain inline editing
on the index view.
Fortunately, we can leverage variants in concert with parameters to conditionally render our Turbo Frames based on specific context.
First, we can update our _post partial by having it link to the edit view,
but with a query string of ?variant=inline.
--- a/app/views/posts/_post.html.erb
+++ b/app/views/posts/_post.html.erb
@@ -6,7 +6,7 @@
</p>
<td>
- <%= link_to "Edit", edit_post_path(post) %>
+ <%= link_to "Edit", edit_post_path(post, variant: :inline) %>
<%= link_to "Show this post", post, data: { turbo_frame: "_top" } %>
</td>
</article>
This means that when the request is made, we’ll have the additional context about how we want to render this response.
Now that we’ve encoded the context into the URL, we need to do something with
it. We can start by first creating a new variant for the edit view that
will include the turbo-frame.
# app/views/posts/edit.html+inline.erb
<% content_for :title, "Editing post" %>
<h1>Editing post</h1>
<%= turbo_frame_tag dom_id(@post) do %>
<%= render "form", post: @post %>
<%= link_to "Cancel", :back %>
<% end %>
Since we’re loading the form on this page, we can conditionally set a hidden
field to capture this value and pass it over to the #update action so it is
informed of the context as well.
--- a/app/views/posts/_form.html.erb
+++ b/app/views/posts/_form.html.erb
@@ -21,6 +21,10 @@
<%= form.text_area :body %>
</div>
+ <% if params[:variant] == "inline" %>
+ <%= hidden_field_tag :variant, "inline", readonly: true %>
+ <% end %>
+
<div>
<%= form.submit %>
</div>
Now that we have a variant responsible for including the turbo-frame in a
variant, we can remove it from the base edit view.
--- a/app/views/posts/edit.html.erb
+++ b/app/views/posts/edit.html.erb
@@ -2,11 +2,7 @@
<h1>Editing post</h1>
-
-<%= turbo_frame_tag dom_id(@post) do %>
- <%= render "form", post: @post %>
- <%= link_to "Cancel", :back %>
-<% end %>
+<%= render "form", post: @post %>
<br>
Now we just need to apply the same changes to the show views so that the
update action can conditionally render the appropriate variant based on
the query parameter.
Similar to the above, we can create a variant for the show view that will
contain a turbo-frame.
# app/views/posts/show.html+inline.erb
<%= turbo_frame_tag dom_id(@post) do %>
<%= render @post %>
<% end %>
This means we can remove it from the base show view.
--- a/app/views/posts/show.html.erb
+++ b/app/views/posts/show.html.erb
@@ -1,12 +1,10 @@
<p style="color: green"><%= notice %></p>
-<%= turbo_frame_tag dom_id(@post) do %>
- <h1><%= @post.title %></h1>
- <p>
- <%= @post.body %>
- </p>
- <%= link_to "Edit", edit_post_path(@post) %>
-<% end %>
+<h1><%= @post.title %></h1>
+<p>
+ <%= @post.body %>
+</p>
+<%= link_to "Edit", edit_post_path(@post) %>
<div>
<%= link_to "Back to posts", posts_path %>
Now that we’ve modified the views, we need to update our controller to conditionally chose the correct variant based on the parameters.
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -1,5 +1,6 @@
class PostsController < ApplicationController
before_action :set_post, only: %i[ show edit update destroy ]
+ before_action :set_variant, only: %i[ show edit update ]
# GET /posts or /posts.json
def index
@@ -8,6 +9,7 @@ class PostsController < ApplicationController
# GET /posts/1 or /posts/1.json
def show
+ request.variant = @variant
end
# GET /posts/new
@@ -17,6 +19,7 @@ class PostsController < ApplicationController
# GET /posts/1/edit
def edit
+ request.variant = @variant
end
# POST /posts or /posts.json
@@ -33,7 +36,7 @@ class PostsController < ApplicationController
# PATCH/PUT /posts/1 or /posts/1.json
def update
if @post.update(post_params)
- redirect_back_or_to post_url(@post), notice: "Post was successfully updated."
+ redirect_to post_url(@post, variant: @variant), notice: "Post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
@@ -56,4 +59,8 @@ class PostsController < ApplicationController
def post_params
params.require(:post).permit(:title, :body)
end
+
+ def set_variant
+ @variant ||= :inline if params[:variant] == "inline"
+ end
end
With this change in place, making edits on the index view returns the teaser
content.

This change also means making edits from the edit page no longer happen
inline, as made evident by the presence of the flash message.

Wrapping Up
Turbo Frames require a new mental model when it comes to managing the state of a page. That, plus that fact that it’s a relatively new technology means that we’re still exploring solutions to common problems as a community.
In this case, Turbo does not offer an off-the-shelf solution to conditionally rendering Frames, but Rails does. I hope that moving forward, this post will serve as guide when others are faced with the same problem.