Why Does Turbo Replace the Outer Frame Instead of Only the Inner Turbo Frame in Rails?

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.