How to redirect from a form that is inside a turbo frame?

I’m using turbo-rails library with Rails 7.0. I have a page that shows a list of users in a group and a form for adding a new user by email. The form is inside a turbo frame. When form is submitted there are two choices:

  1. If the email is incorrect it will show the form with a validation error “User not found” by rendering the form partial only and replacing the turbo-frame’s content.

  2. Otherwise, if the user is found, it will add the user to the group and re-render the entire page using the Rails redirect_to function.

#1 works, but #2 does not. Rails responds with the redirect but the page is not updated (i.e. I don’t see a new user added to the list). How do I make both #1 and #2 work? Is there a better approach to achieve what I want? Is there a parameter for redirect_to method to break out of the turbo frame?

My implementation

The form for adding a new user is inside a Rails partial _add_user_form.html.erb:

<turbo-frame id='add_user_form'>
    <form action="/group/123/add_user" method="post">
         <!-- some validation -->
        <input type="email" name="email" />
        <input type="submit" name="Add user" />
    </form>
</turbo-frame>

The view groups/users.html.erb renders the partial and the list of users in the group:

<h1>Users</h1>

<!-- renders the form from _add_user_form.html.erb partial  -->

<p>User 1</p>
<p>User 2</p>
<p>User 3</p>

In the Rails controller groups_controler.rb I have

def add_user
  user = User.find_by(email: params[:email])

  if user.nil?
    return render(
      partial: 'add_user_form',
      locals: {
        email: params[:email],
        user_not_found: true
      }
    )
  end

  group = Group.find(params[:id])
  group.users << user  # Add the user to the group
  redirect_to users_group_path(group)
end

Solution that sucks

Of course, I can get rid of the turbo frame, and re-render the entire page when form is submitted (with validation errors or not). But I hoped to use the turbo frame functionality, so I only render the form on validation error and keep the rest of the page unchanged.

3 Likes

There was a similar question posted here that i forgot the link to it. But, basically you needed to use a Stimulus Controller to listen for the events.

When the form has validation errors, i.e status: :unprocessable_entity it will do nothing. However, if the response status is success, it visits the ur.

// form_redirect_controller.js

import { Controller } from "@hotwired/stimulus"
import * as Turbo from "@hotwired/turbo"

export default class extends Controller {
  next(event) {
    if (event.detail.success) {
      const fetchResponse = event.detail.fetchResponse

      history.pushState(
        { turbo_frame_history: true },
        "",
        fetchResponse.response.url
      )

      Turbo.visit(fetchResponse.response.url)
    }
  }
}

Wiring it up with your html

<turbo-frame id='add_user_form'>
    <form action="/group/123/add_user" method="post" data-controller='form-redirect' data-action='turbo:submit-end->form-redirect#next'>
         <!-- some validation -->
        <input type="email" name="email" />
        <input type="submit" name="Add user" />
    </form>
</turbo-frame>
3 Likes

Thanks @rockwell! This is an excellent workaround, since I can write this Stimulus controller once and reuse it in my forms. Having said that, to me this solution feels a bit … hacky. I wish there was a built-in way to conditionally break out of the turbo frame programmatically, from a Rails controller for example.

2 Likes

If it were me, I’d drop the turbo-frame and use turbo-stream with http instead. No additional javascript needed.

def add_user
  @user = User.find_by(email: params[:email])
  @group = Group.find(params[:id])

  if user.nil?
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to users_group_path(group) }
    end
  else
    group.users << user  # Add the user to the group
    redirect_to users_group_path(group)
  end
end
<!-- add_user.turbo_stream.erb -->
<%= turbo_stream.update 'add_user_form' %>
    <form action="/group/123/add_user" method="post">
         <!-- some validation -->
        <input type="email" name="email" />
        <input type="submit" name="Add user" />
    </form>
<% end %>
2 Likes

Your solution looks very nice! When you say

I’d drop the turbo-frame

do you mean to NOT have <turbo-frame id='add_user_form'> in the html at all? If so, how will turbo_stream.update 'add_user_form' work? Doesn’t it reference the ID of the turbo frame to update?

Sorry, I should’ve added an id the form. The turbo-stream will look for the DOM element with the ID, and the perform the action (replace, update, delete, etc).

Original Form:

    <form action="/group/123/add_user" method="post" id="add_user_form">
         <!-- some validation -->
        <input type="email" name="email" />
        <input type="submit" name="Add user" />
    </form>

add_user.turbo_stream.erb using replace

<%= turbo_stream.replace 'add_user_form' %>
    <form action="/group/123/add_user" method="post" id="add_user_form">
         <!-- some validation -->
        <input type="email" name="email" />
        <input type="submit" name="Add user" />
    </form>
<% end %>

or using update

<%= turbo_stream.update 'add_user_form' %>
  <input type="email" name="email" />
  <input type="submit" name="Add user" />
<% end %>

I recommend reading the documentation on how turbo-streams work.

1 Like

The turbo-stream will look for the DOM element with the ID

Oh so the first argument of turbo_stream.replace method can be an ID of ANY element, not just limited to a turbo-frame. I did not know that, thanks!

I recommend reading the documentation on how turbo-streams work.

I will do that!

Totally genius. I’ve been looking for something like this for a while. Thanks for your post. I don’t see how this would be considered hacky. It uses Stimulus JS called from a callback built into Turbo. HOTWIRE is composed of Turbo, Stimulus JS and soon Strada. So combining Stimulus with Turbo is per the documentation.

1 Like

I’ve registered here just to thank you for that great point!

I’ve been having issues with turbo frames over and over. Last one is about the update that doesn’t allow to break out of the frame (Turbo Handbook). I.e. I submit the form in that frame, and after it’s successfully submitted I want to do redirect and reload the whole page, which became super complex to achieve with turbo frames.

So dropping turbo-frame and using just turbo-stream made my life so much easier and the code so much cleaner and stable. Yay!