5

I have

  • 1: a link in a turbo frame which loads a form into the same frame. Working Well
  • 2: the form if it is not valid then should only update itself by marking the missing fields with errrors. Working Well
  • 3: in case the form submission is successfull, I should redirect, that is not working well, because it is rendering the result of the redirect in as TURBO_STREAM, somehow I should break out to top in that case.

Basically this is the code:

- list_of_projects...
= turbo_frame_tag 'new_project'
  = link_to "New Project", new_project_path

then in views/projects/new.html.slim I have:

= turbo_frame_tag 'new_project' do
  = simple_form_for @project_form, url: projects_path do |form|
...

Then in the controller:

  def create
    @project_form = ProjectForm.new project_params
    if @project_form.valid?
      command_bus.(Conversations::Commands::CreateProject.new(id: SecureRandom.uuid,
                                                          title: @project_form.title))

      # should redirect without AS Turbo
      redirect_to projects_url

Any ideas how to do it in a reusable manner?

Boti
  • 3,275
  • 1
  • 29
  • 54

2 Answers2

5

This is "new" in Turbo v7.3.0 (turbo-rails v1.4.0), redirect behavior when frame is missing is reverted to the original way of forever staying in a frame.

There is a turbo:frame-missing event emitted so that you could customize this behavior:

turbo:frame-missing - fires when the response to a <turbo-frame> element request does not contain a matching <turbo-frame> element. By default, Turbo writes an informational message into the frame and throws an exception. Cancel this event to override this handling. You can access the Response instance with event.detail.response, and perform a visit by calling event.detail.visit(...)
https://turbo.hotwired.dev/reference/events

Like this:

document.addEventListener("turbo:frame-missing", (event) => {
  const { detail: { response, visit } } = event;
  event.preventDefault();
  visit(response.url);
});

While this works, it does one more request to the server, which is what used to happen way back.

If you want to just display the redirected response, you can visit the response:

document.addEventListener("turbo:frame-missing", (event) => {
  const { detail: { response, visit } } = event;
  event.preventDefault();
  visit(response); // you have to render your "application" layout for this
});

Turbo frame requests used to render without a layout, they now render within a tiny layout. response has to be a full page response to be visitable, otherwise, turbo will refresh the page, which makes it even worse. This would fix it:

def show
  render layout: "application"
end

Custom turbo stream redirect solution:

https://stackoverflow.com/a/75750578/207090

I think it's simpler than the solution below.


Set a custom header

This lets you choose on the front end if and when you want to break out of the frame.

Set a data attribute with a controller action name, like data-missing="controller_action" (or any other trigger you need, like, controller name as well):

<%= turbo_frame_tag "form_frame", data: { missing: "show" } do %>
#                                                   ^
# this is where missing frame is expected, it's not strictly 
# necessary, but it's where "application" layout is required

This was more of a "i wonder if that would work" type of a solution, just make sure you need it:

// app/javascript/application.js

addEventListener("turbo:before-fetch-request", (event) => {
  const headers = event.detail.fetchOptions.headers;
  // find "#form_frame[data-missing]"
  const frame = document.querySelector(`#${headers["Turbo-Frame"]}[data-missing]`);
  if (frame) {
    // if frame is marked with `data-missing` attribute, send it with request
    headers["X-Turbo-Frame-Missing"] = frame.dataset.missing;
  }
});

addEventListener("turbo:frame-missing", (event) => {
  const { detail: { response, visit } } = event;
  if (response.headers.get("X-Turbo-Frame-Missing")) {
    // if response has "frame missing" header it can be rendered
    // because we'll make sure it's rendered with a full layout
    event.preventDefault();
    visit(response);
  }
});
# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  layout -> {
    if turbo_frame_request?
      # check if we are in an action where a missing frame is
      # if you're jumping between controllers, you might need to
      # have controller name in this header as well
      if request.headers["X-Turbo-Frame-Missing"] == action_name
        # let `turbo:frame-missing` response handler know it's ok to render it
        headers["X-Turbo-Frame-Missing"] = true
        # because it's a full page that can be displayed
        "application"
      else
        "turbo_rails/frame"
      end
    end
  }
end

https://turbo.hotwired.dev/handbook/frames#breaking-out-from-a-frame

https://github.com/hotwired/turbo/pull/863

Alex
  • 16,409
  • 6
  • 40
  • 56
2

It depends on the kind of experience you are trying to achieve, but it seems to me that the easiest way to make this work is to specify _top as the turbo frame when submitting the form. It can be set on the button or the form.

= turbo_frame_tag 'new_project' do
  = simple_form_for @project_form, url: projects_path do |form|
    ...
    form.button :submit, "Save", data: {turbo_frame: "_top"}

Then in your controller, do the redirect as normal.

Andrew
  • 227,796
  • 193
  • 515
  • 708