Break out of a frame during form redirect

Yup. It does work with 303 if the form is out of the frame. I am trying to combine the both. Having the grace of frames to show the validation errors without re-rendering the whole page. But at the same time break out of a frame when the form submission is successful.

I understand that maybe the frames are not designed to handle this use case. But I am curious to know how others are handling situations like this

2 Likes

Sorry, I wasn’t clear enough. I meant the request was made from out of a turbo frame:

<%= turbo_frame_tag "new_discussion_message", src: new_discussion_message_path(@discussion), target: "_top" %>
<%= turbo_frame_tag "new_discussion_message", target: "_top" do %>
  <%= form_with model: [@discussion, @message], class: 'new-discussion__form push-double--top d-flex flex-column' do |f| %>
    <%= f.hidden_field :creator_id %>
    <%= f.rich_text_area :content, data: { controller: "signatured-input", signatured_input_signature: Current.user.signature.to_s } %>

    <footer class="new-discussion__footer">
      <p class="push--bottom" style="font-size: 1.8rem;">
        <%= image_tag @discussion.parent.client.avatar_url %>
        <%= @discussion.parent.client.name %> will receive your message in their email.
      </p>

      <%= f.button class: 'btn btn-primary btn--with-icon', type: 'submit', data: { "disable_with": "Sending…" } do %>
        Reply
      <% end %>
    </footer>
  <% end %>
<% end %>
def create
  @message = @discussion.messages.new(message_params)

  respond_to do |format|
    if @message.save
      format.any(:html, :js) { redirect_to @discussion.parent.client, flash: { follow_up_with_new_task: true } }
    end
  end
end

This, however, doesn’t cover the case where @message.save fails the validations. Though you could send a turbo_stream on validation failure to render the errors in your frame.

1 Like

Is there any upside of keeping this form inside a frame when its always targeting the _top?. Just curious.

3 Likes

I haven’t played much with Turbo yet but couldn’t you handle this scenario with Turbo Stream instead of Turbo Frame? You just display the login error message with a Turbo Stream and redirect with Turbo Drive if the login is successful.

The pattern of re-rendering a form with validation errors and redirecting on success is very common - is there a reason Turbo Drive prevents this? What is the simplest workaround here?

See also: https://github.com/hotwired/turbo/issues/22. This seems to be quite a serious breaking bug, especially as there is no easy way to disable Turbo in forms (data-turbo=“false” does not work with links, and the Turbo API does not appear to allow insertion of HTML as with the old Turbolinks.controller.cache.put method).

Regarding form validation errors:

1 Like

I’m trying to do exactly the same.

Either the JS will raise if the frame has a target _top and the backend does not redirect, either the frame has no target but nothing will happen on redirect.

Try adding data-turbo-frame="_top" to your form(not the frame). I tried this and it works.
If you’re using rails form builder: form_with(..., data: {'turbo-frame' => '_top'})

1 Like

Adding data-turbo-frame="_top" to your form has bad side effect that page jump to top after form submission. Especially visible on long pages on mobile.

I “solved” it by having

.mt-5.md:mt-0.md:col-span-2 id=dom_id(current_user)
  = form_with(model: current_user, url: profiles_path, data: {turbo_frame: "_top"}) do |f|

and then in the controller

  def update
    if current_user.update(profile_params)
      flash[:notice] = "Profile successfully updated."
      redirect_to controller: "shots", action: :index
    else
      respond_to do |format|
        format.turbo_stream { render turbo_stream: turbo_stream.replace(current_user, partial: "form") }
        format.html { render :edit }
      end
    end
  end

But this seems like a very standard type of action so I was expecting Turbo to work out of the box when getting a redirect :thinking:

2 Likes

now that the new version of turbo is listening to 422 responses, for me it works like this:

I have a standard rails form (not wrapped in a turbo_frame_tag), like so:

= form_with(model: user) do |form|
  ...

then, a standard rails controller with an action, like so:

def create
  @user = User.new(user_params)
  if @user.save
    redirect_to users_path, status: 303, notice: 'User was successfully created.'
  else
    render :new, status: :unprocessable_entity
  end
end

The important bits are the 303 status code for the redirect, and the status :unprocessable_entity to rerender the form with validation errors.

If you have a setup like this, you don’t need to wrap your forms into a frame. Turbo takes care of rerendering the form with validation errors, and you can redirect to any path you’d like!

4 Likes

I solved this using Stimulus and not using target="_top".

In my case I needed to use a <turbo-frame> as I was loading the form in from another controller. Think of the way you rename a contact on Hey.com

Form errors are handled automatically by returning a 422 status code.

For form success I listen to turbo:submit-end on the turbo frame and Turbo.visit() the next page.

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


export default class extends Controller {

  static values = { next: String }

  next(event) {
        if (event.detail.success) {
            Turbo.visit(this.nextValue)
        }
  }
}
<a data-turbo-frame="contact-name-edit"  href="/contact/1/name/edit">Load form</a>
<turbo-frame id="contact-name-edit" data-controller="form" data-form-next-value="/contact/1" data-action="turbo:submit-end->form#next"></turbo-frame>

My original post on this Forms without redirect - #24 by nwjlyons

2 Likes

data-turbo-frame => ‘_top’ fix form redirection but suppress validation message. any advice please.

= turbo_frame_tag dom_id(@receipt)
  = form_for @receipt, data: {'turbo-frame' => '_top'} do |f|
    - if @receipt.errors.any?
      #error_explanation
        h2 = "#{pluralize(@receipt.errors.count, "error")} prohibite
        ul
          - @receipt.errors.full_messages.each do |message|
            li = message

So the best solution that I have found was this. Do not use turbo_frame_tag. The redirect process is not working the way it should. Meaning, when you redirect it should redirect to the new page, that’s the intent of that command.

Right Turbo, on redirect, tries to replace a turbo_frame_tag that it came from. And if it doesn’t find it after it processes HTML from redirected page, it fails to “redirect” to the new page. Maybe, the solution should be this: if frame_tag is present, replace it, if not replace the whole page.

For now, here’s the workaround that works best:

Instead of using turbo_frame_tag. create a new.turbo_stream.erb file for your form.

Wrap your form into a div with id. And use turbostream to replace that part of the page.

_form.html.erb

<%= tag.div "new_board_form" do %>
   <%= form_with board, url: new_board_path do %> 
   <% end %>
<% end %>

new.turbo_stream.erb ← to render errors

<%= turbo_stream.replace "new_board_form" do %>
  <%= render "form", board: @new_board %>
<% end %>

And then simple redirect on successful form completion

boards_controller.rb

   if @new_board.save
      redirect_to board_path (@new_board) }    
    else
      render :new
    end
2 Likes

There is discussion on GitHub at Redirect to new page on successful form submission, rerender otherwise · Issue #138 · hotwired/turbo · GitHub.

I did a hack partial solution on GitHub Redirect to new page on successful form submission, rerender otherwise · Issue #138 · hotwired/turbo · GitHub

Maybe we could continue there?

Agrees with this solution. I’m using same. The most annoying part is that I have to write turbo_stream.replace. WTF? why Turbo then?! I’ve been using same technique with jQuery. Why then I need Turbo :smiley:. I hope it will be changed in future Turbo versions.

1 Like

Is this still a problem in the current version of Turbo? I’m running into this same issue. I want to re-render within the turbo frame except for when I want to break out and redirect. Setting the container frame target to _top doesn’t work because it applies to every request. I was thinking it would be nice if I could change/specify the turbo frame in the response header, or something like this:

redirect_to users_path, status: :see_other, turbo_frame: "_top"

Is something like this possible?

1 Like

@andrewhavens This is exactly what I was wondering as well… have you found an answer? :smile:

@zoopzoop No, I have not found an answer yet.