Problem Description:
I am encountering an issue with Turbo Frames in my Rails application, specifically when working with nested Turbo Frames. I have an outer Turbo Frame (:test
) and several inner Turbo Frames (:name
, :byline
, etc.) in a show page. When I submit the form from an edit
page and encounter validation errors, I expect only the inner Turbo Frame (where the form resides) to be replaced with the validation errors, without affecting the entire outer frame.
However, instead of updating just the inner Turbo Frame, Turbo replaces the entire outer Turbo Frame (:test
), causing the form to be lost and the view to reset. This behavior works as expected when the inner Turbo Frame is not wrapped in any other Turbo Frame.
Here are the details of the files involved and the setup:
Show Page (show.html.erb
):
<%= turbo_frame_tag :test do %>
<section class="grid gap-2 max-w-prose m-auto">
<%= link_to "Edit Article", edit_article_path(@article) %>
<%= render "inline_edit", model: @article, method: :name do %>
<h1><%= @article.name %></h1>
<% end %>
<%= render "inline_edit", model: @article, method: :byline do %>
<span>By: <%= @article.byline %></span>
<% end %>
<%= render "inline_edit", model: @article, method: :published_on do %>
<% if @article.published_on.nil? %>
<span>(Unpublished)</span>
<% else %>
<%= localize @article.published_on, format: :long %>
<% end %>
<% end %>
<%= render "inline_edit", model: @article, method: :category_ids do %>
<strong>Categories</strong>
<span>
<% @article.categories.each do |category| %>
<span><%= category.name %></span>
<% end %>
</span>
<% end %>
<%= render "inline_edit", model: @article, method: :content do %>
<%= @article.content %>
<% end %>
</section>
<% end %>
Edit Page (edit.html.erb
):
<%= form_with model: @article, class: "grid gap-2 max-w-prose m-auto" do |form| %>
<%= link_to "Back", article_path(@article) %>
<%= render "inline_fields", form: form, method: :name do %>
<%= form.label :name %>
<%= form.text_field :name %>
<% end %>
<%= render "inline_fields", form: form, method: :byline do %>
<%= form.label :byline %>
<%= form.text_field :byline %>
<% end %>
<%= render "inline_fields", form: form, method: :published_on do %>
<%= form.label :published_on %>
<%= form.date_field :published_on %>
<% end %>
<%= render "inline_fields", form: form, method: :category_ids do %>
<fieldset>
<legend>
<%= @article.class.human_attribute_name(:category_ids) %>
</legend>
<%= form.collection_check_boxes :category_ids, Category.all, :id, :name do |builder| %>
<%= builder.check_box %>
<%= builder.label %>
<% end %>
</fieldset>
<% end %>
<%= render "inline_fields", form: form, method: :content do %>
<%= form.label :content %>
<%= form.rich_text_area :content %>
<% end %>
<%= form.button %>
<% end %>
Inline Edit Partial (_inline_edit.html.erb
):
<% frame_id = dom_id(model, "#{method}_turbo_frame") %>
<%= form_with model: model, class: "contents" do %>
<turbo-frame id="<%= frame_id %>" class="contents group inline-edit">
<%= yield %>
<%= link_to edit_polymorphic_path(model) do %>
Edit <%= model.class.human_attribute_name(method) %>
<% end %>
</turbo-frame>
<% end %>
Inline Fields Partial (_inline_fields.html.erb
):
<% frame_id = dom_id(form.object, "#{method}_turbo_frame") %>
<turbo-frame id="<%= frame_id %>" class="contents">
<%= yield %>
<% if form.object.errors.any? %>
<ul class='gap-2 items-center flex'>
<% form.object.errors.each do |error| %>
<li class=""><%= error.full_message %></li>
<% end %>
</ul>
<% end %>
<%= form.button class: "hidden group-inline-edit:inline" do %>
Save <%= form.object.class.human_attribute_name(method) %>
<% end %>
<%= link_to "Cancel", polymorphic_path(form.object), class: "hidden group-inline-edit:inline" %>
</turbo-frame>
Articles Controller (articles_controller.rb
):
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
def show
@article = Article.find params[:id]
end
def edit
@article = Article.find params[:id]
end
def update
@article = Article.find params[:id]
if @article.update article_params
redirect_to article_path(@article)
else
render :edit, status: :unprocessable_entity
end
end
private
def article_params
params.require(:article).permit(
:byline,
:content,
:name,
:published_on,
category_ids: []
)
end
end
Expected Behavior:
- When the form inside the
:inline_edit
Turbo Frame is submitted and validation fails, only the relevant Turbo Frame (the one corresponding to the form being edited) should be updated with validation errors. - The rest of the page (including the outer
:test
Turbo Frame) should remain unaffected.
Actual Behavior:
- Instead of updating just the inner Turbo Frame, Turbo replaces the entire outer Turbo Frame (
:test
) when there are validation errors, causing the form to be reset and lost.
Troubleshooting Attempts:
- Each Turbo Frame has a unique
id
based on the model and field. - The
data-turbo-frame
attribute in the form action seems to be set correctly. - When the outer Turbo Frame (
:test
) is removed, the behavior works as expected, and only the inner Turbo Frame is updated with errors.
Question:
Why does Turbo replace the entire outer frame (:test
) instead of only updating the inner Turbo Frame (:inline_edit
) when there is a validation error, specifically when the inner Turbo Frame is nested within the outer frame? This behavior works as expected when the :inline_edit
Turbo Frame is not wrapped in any other Turbo Frame. Is this a known issue with Turbo Frames in nested structures, or is there a configuration issue? How can I ensure that only the inner Turbo Frame gets updated in such scenarios?
Obs: I m using this repository: GitHub - thoughtbot/hotwire-example-template at hotwire-example-inline-edit, to implements edit inline fields in my project.