Cómo utilizar turbo_frame_tag en un layout personalizado

Me encuentro trabajando en un side project utilizando Rails 7 y Hotwire, una de las funciones que implementé fue una ventana modal la cual es llamada utilizando turbo_frame_tag para hacer el render del formulario de creación. Todo iba bien hasta que decidí incluir un layout personalizado para el dashboard del usuario con sesión.

TLDR;

No cargues el layout de manera estática usando layout 'custom_layout'. En su lugar, utiliza un método que revise si la solicitud es un 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

El problema

Inicialmente la lógica para cargar la ventana modal estaba compuesta de 3 elementos:

Una llamada al turbo_frame_tag en el layout base que contiene mi aplicación de rails

# 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>

Una vista la cual contiene el contenido del frame que representa la 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 %>

Y finalmente una llamada al turbo frame utilizando un data attribute:

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

Esto por defecto lo que hace es generar el turbo-frame y hacer render de este al momento en mi aplicación utilizando las propiedades de 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>

El problema inició al crear un nuevo layout idéntico dentro del directorio layouts y colocandolo como el layout por defecto dentro del ApplicationController de mi namespace

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

Inmediatamente al hacer esto, al momento de dar click en el enlace que llama al turbo frame en lugar de recuperar <turbo-frame id="modal"> como respuesta del request, comencé a recibir todo el layout como respuesta; lo que provocó que la acción nativa de turbo-rails dejara de funcionar:

<!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" />

La solución

Después de investigar un rato y terminar en este issue reportado en 2021 en el repositorio de hotwired/turbo-rails encontré que la solución es verificar si el request viene clasificado como turbo_frame_request? y si es así devolver false para evitar que la respuesta incluya el el layout personalizado que agregamos, si tienes curiosidad de como funciona este método turbo_frame_request? puedes revisarlo en este enlace, prácticamente estamos verificando si el request contiene el header Turbo-Frame.

La solución puede quedar de la siguiente manera

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

El 5 de Junio de 2023 se abrió un Pull request que agrega una sección al readme para explicar esta situación y como solucionarlo para no terminar navegando entre issues para encontrar como abordar este problema.