Hotwire Discussion

Break out of a frame during form redirect

  • I began by moving my forms to a frame.
  • On error, the server redirects back to the form with errors. Frames will replace the bounding HTML. All good till now
  • However, after success, I want to move to a different page all together and that page cannot have the same frame in it’s HTML.

A great example of this is a login page. After login, I want to redirect the user to the dashboard vs keeping them on the same login page with a success message.

So is it possible to use some Content-type or some header to instruct turbo that this time just replace the entire page?

1 Like

Perhaps target="_top" is what you’re looking for, more on it here:

Thanks for the reply :slight_smile:

Not sure.

  • The form is on GET /login. This page holds the frame for the form
  • Request goes to POST /login.
  • It issues a redirect to GET /dashboard. This page does not have the frame. So there is no way to add any attribute
1 Like

Gotcha. How about this? This worked for me where a POST request (made out of a frame) that responded with a redirect would make turbo navigate to the redirect URL.

Turbo Drive handles form submissions in a manner similar to link clicks. The key difference is that form submissions can issue stateful requests using the HTTP POST method, while link clicks only ever issue stateless HTTP GET requests.

After a stateful request from a form submission, Turbo Drive expects the server to return an HTTP 303 redirect response, which it will then follow and use to navigate and update the page without reloading.

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

1 Like

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.

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'})

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:

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!

3 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

1 Like

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

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?