How to use turbo_frame_tag in a custom layout

I am working on a side project using Rails 7 and Hotwire. One of the features I implemented was a modal window which is called using turbo_frame_tag to render the creation form. Everything was going well until I decided to include a custom layout for the logged-in user dashboard.

TLDR;

Do not load the layout statically using layout 'custom_layout'. Instead, use a method that checks if the request is a turbo_frame_tag.

class Foo::ApplicationController < ApplicationController

  layout :set_custom_layout_with_turbo_frame_support

private

  def set_custom_layout_with_turbo_frame_support
    return 'custom_layout' unless turbo_frame_request?
  end
end

The problem

Initially, the logic to load the modal window was composed of 3 elements:

A call to the turbo_frame_tag in the base layout that contains my Rails application:

# app/views/layouts/application.html.erb
<body>
  <%= render "shared/alerts" %>
  <main class="container mx-auto mt-28 px-5 flex">
  <%= turbo_frame_tag "modal" %>
    <%= yield %>
  </main>
</body>

A view that contains the content of the frame representing the modal:

# app/views/elements/new.html.erb
<%= turbo_frame_tag "modal" do %>
  <div class="modal modal-open", data-controller="turbo-modal", data-action="...">
    <%= form_with(model: @element, url: element_path, class: 'modal-box') do |form| %>
      My form content
    <% end %>
  </div>
<% end %>

And finally a call to the turbo frame using a data attribute:

<%= link_to 'Create Element', new_element_path, class: 'link link-hover', data: { turbo_frame: 'modal' } %>

By default, this generates the turbo-frame and renders it in my application using the properties of turbo-rails.

<turbo-frame id="modal">
  <div class="modal modal-open",
       data-controller="turbo-modal",
       data-action="....">
    <form class="modal-box" action="/elements" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="xyz" autocomplete="off" />
      My form content
    </form>
    </div>
</turbo-frame>

The problem started when I created a new identical layout within the layouts directory and set it as the default layout within the ApplicationController of my namespace.

class Foo::ApplicationController < ApplicationController
  layout 'custom_layout'
end

Immediately upon doing this, when I clicked on the link that calls the turbo frame, instead of retrieving <turbo-frame id="modal"> as the response of the request, I started receiving the entire layout as the response. This caused the native turbo-rails action to stop working:

<!DOCTYPE html>
<html data-theme="light">
  <head>
    <title>Foo</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="csrf-param" content="authenticity_token" />
    <meta name="csrf-token" content="5QJ-_2XaOHFTaNKEJkLcjOTewSbJ03QMjp6yeVABCkHaP94Uc4la6GnGtxIRp6LmoXAwbpixj657sYrc3ir2Mg" />


    <link rel="stylesheet" href="/assets/tailwind-24d55e02196000307cecef67bba818aa60975a400a31c502e1ad9c155bd7b147.css" data-turbo-track="reload" />
    <link rel="stylesheet" href="/assets/inter-font-8c3e82affb176f4bca9616b838d906343d1251adc8408efe02cf2b1e4fcf2bc4.css" data-turbo-track="reload" />

    <link rel="stylesheet" href="/assets/application-186311012c4e733c61b267717a8abc6092b57cc4ba1b125e36e9c3fc2c3b8e30.css" data-turbo-track="reload" />

The solution

After researching for a while and ending up on this issue reported in 2021 in the hotwired/turbo-rails repository, I found that the solution is to check if the request is classified as turbo_frame_request? and if so, return false to prevent the response from including the custom layout we added. If you’re curious about how this turbo_frame_request? method works, you can check it out at this link. Essentially, we’re checking if the request contains the Turbo-Frame header.

The solution can be as follows

class Foo::ApplicationController < ApplicationController

  layout :set_custom_layout_with_turbo_frame_support

private

  def set_custom_layout_with_turbo_frame_support
    return 'custom_layout' unless turbo_frame_request?
  end
end

On June 5 2023 a Pull request was opened adding a section to the readme to explain this situation and how to solve it, in order to avoid having to navigate through issues to find a way to address this problem.