Form redirects not working as expected

Hey all,

I’ve been integrating turbo-rails into my Rails application and things have been moving along just fine.

I’m running into an issue specifically with a form that’s submitted with valid data. The form is supposed to redirect away from /projects/new and over to the “show” page such as projects/1.

I’ve actually recreated the issue with the example repo below. It contains the most bare-bones reaction of the behavior. No models, data, etc.

If you look at projects_controller.rb, you’ll see:

class ProjectsController < ApplicationController
  def new
  end

  def create
    # The official docs mention a 303 as the status code to use.
    # Neither of the redirect_to calls below work.

    redirect_to project_path(1)
    # redirect_to project_path(1), status: 303
  end

  def show
  end
end

And the form rendered by the “new” action is:

<h1>Projects#new</h1>
<p>Find me in app/views/projects/new.html.erb</p>

<%= turbo_frame_tag "new_project" do %>
  <%= form_with url: projects_path, method: :post do |f| %>
    <%= f.submit "Submit" %>
  <% end %>
<% end %>

So I have my “new_project” turbo frame which is great. It lets me re-render just that frame when a user submits a form with errors and those errors need to be show.

Now I’ve removed all that logic from create. All that happens is create redirects over to project_path(1) just to demonstrate the behavior. So I would expect that a form submission would trigger a fetch request from the client to fetch the “show” page and render it. That’s documented here in the official guide.

If you visit /projects/new and click “Submit”, you’ll see the request is made. The screenshot below shows that form submission and the subsequent fetching of /projects/1. You can see that here:

The problem though is that the response from /projects/1 is not rendered and the console shows an error.

Now the error message is correct. There is no turbo-frame element with the new_project id in the response. It’s a completely different page without the form. It has no turbo-frame elements.

I’m not sure what the solution is here. I’ve read up on (and played around with) setting the target to _top to break out of the frame. That’s not what I want as I want the frame behavior when users submit the form with invalid data.

Any thoughts on this? Thanks for your time. The forms here have been valuable over the last week.

- Andrew

3 Likes

Hey Andrew,
I saw the same problem for one of the forms that required me to just redirect to a new page.

From what I saw in the code, Turbo will only redirect on GET requests. I agree with you that this is a pretty strange case and we definitely need a way to say in our form itself that if we get redirected in response, we can do a full redirect. If someone can help with that, would be great!

For now, I did a hack with stimulus. I can share my

import { Controller } from "stimulus"
import { Turbo } from "@hotwired/turbo-rails"

export default class extends Controller {
  static targets = [ "form" ]

  initialize() {
    this.listener = this.listener.bind(this)
  }

  connect() {
    this.formTarget.addEventListener("turbo:submit-end", this.listener)
  }

  disconnect() {
    this.formTarget.removeEventListener("turbo:submit-end", this.listener)
  }

  listener(event) {
    const fetchResponse = event.detail.fetchResponse
    if (fetchResponse && fetchResponse.response.status === 201) {
      Turbo.visit(fetchResponse.response.headers.get('Url'))
    }
  }
}

Then in my .erb template I do this for forms where full redirect is required:

<%= turbo_frame_tag 'banana_form' do %>
 <%= form_with url: bananas_path, data: { controller: 'redirect-on-created', redirect_on_created_target: 'form' } do |f| %>
   <%= f.submit 'Go Bananas' %>
  <% end %>
<% end %>

In this case, if your controller will do something like this:

def create
    @banana = Banana.new(banana_params)
    if @banana.valid?
      head :created, url: bananas_path
    else
      render turbo_stream: turbo_stream.replace('banana_form', partial: 'form')
    end
end

So if you will have errors - it will replace your form with a fresh version of partial, if not it will return info for redirect and Turbo will do the job.

I know it’s probably hacky way and I would love Hotwire docs to answer this question for me, but for now, it’s working just fine.

1 Like

Thanks for the reply @savroff. Nice to meet you. Thanks for sharing your findings too. Just inspired me to take another crack at this.

I came up with this alternative solution. It’s not perfect and actually doesn’t use turbo-frame at all since it doesn’t seem to be designed for this use case (unless it’s a bug :man_shrugging:).

So I have my project_controller.rb file:

class ProjectsController < ApplicationController
  def new
  end

  def create
    if params[:name] == "valid"
      redirect_to project_path(1)
    else
      @error = "some error"
      render turbo_stream: turbo_stream.replace("new_project", partial: "form")
    end
  end

  def show
  end
end

This is the new.html.erb file:

<h1>Projects#new</h1>
<p>Find me in app/views/projects/new.html.erb</p>

<%= render "form" %>

This is the _form.html.erb file:

<div id="new_project">
  <% if @error.present? %>
    <p><%= @error %></p>
  <% end %>
  <%= form_with url: projects_path, method: :post do |f| %>
    <%= f.text_field :name %>
    <%= f.submit "Submit" %>
  <% end %>
</div>

So for:

  1. Valid data you’ll be redirected as expected.
  2. Invalid data the form (and just the form) will get re-rendered to show the errors.

This okay, but it sort of feels like reacting turbo-frame by just rendering a turbo-stream instead. At least it’s clean and requires no client-side code.

Thoughts?

@andrewjmead nice to meet you as well :blush:

Yeah, agree with you that it still looks like we trying to find a workaround for something that needs to be solved by Turbo itself.

I wonder what is sensible solution here, If redirect does not bring <turbo-frame> nor <turbo-stream> just follow redirect? Render whatever came?

I definitely solves our problem but I wonder if someone would prefer to keep existing behavior.

If you redirect in response to an action within a frame, the place you redirect to has to have a matching frame tag. What you’re describing should indeed work. It’s how the hotwire chat example works as well: hotwire-rails-demo-chat/rooms_controller.rb at main · hotwired/hotwire-rails-demo-chat · GitHub

I can’t find a use-case where it can hurt to do a full redirect if we can’t find a match with any frame tag from a response with our current page. But I would be interested to see what the Basecamp team thinks about this.

2 Likes

Correct me if I’m wrong, but in the demo chat example redirect is working fine, when you need to replace a frame tag only and keep the page context the same.

In my case, for example, I want to use Turbo as part of a form on my main site landing page and once it’s completed - I would love to do a full redirect to another screen where the whole experience of the page will change.

You can say that in this case I just shouldn’t use Turbo at all, but I just LOVE how I can get back a form with errors state via Turbo right now, plus I better use one library everywhere and keep my form consistent.

I know that also an approach with _top frame, but I’m not a fan of it.

In my case I have a kind of a long checkout page. Lot’s of inputs, checkboxes, dropdowns etc. More than two screens on standard mobile phone. In one place I have few dropdowns that change overall price (eg. delivery type, additional services, etc).

With Turbo when dropdowns are changed with small Stimulus controller I can post and redirect to full page with <turbo-frame> with recalculated prices and Turbo will scroll back to place where user was interacting. Fucking magic if network latency is low. And all my users are in same country as my servers so latency is low.

On full submit I check for error and if data are invalid I redirect with full page and <turbo-frame>. If all is OK I wanted to redirect to next step but to my surprise Turbo demands matching frame.

Has anyone solved this? I’m having the same issue — frame won’t get updated after update redirects to show.

show method includes turbo_frame_tag() with the same ID as edit

Thanks for Turbo btw, otherwise it works pretty good so far.

EDIT:
I noticed <turbo-frame [src]> attribute doesn’t change after update method redirects to show

2 Likes

For anyone who’s still looking for a solution, you can use turbo_frame 'fame_id', target: '_top' do

2 Likes

Using target: '_top' destroys the purpose of having the form wrapped in a turbo_frame so that just the form swaps out in the event of errors.

I’ve come to the conclusion for now that I just have to disable Turbo on all forms with a redirect response, which is almost all of them. Target _top and status: :see_other never got the redirect rendering for me. Super frustrating.

1 Like

Hm, that really defeats the purpose of it. I’d instead rely on the default Turbo behaviour - which is to do a replacement of the entire page body, i.e. not bother with turbo frames. If you’re avoiding this because of performance issues - I’d suggest re-thinking what you’re rendering as part of the layout. Maybe offload the heavy parts of it to separate ajax requests after the initial render.

An alternative would be a small Stimulus controller like the one @savroff suggested that would catch the response from the server, and if it’s successful - do a redirect; if not, stay with the default turbo frame behaviour. I think that’s quite a good solution.

I’ve gone this route when rendering modals with forms. Have a read here if you’re interested - How to create seamless modal forms with Turbo Drive | how to ruby .

Either way I think it’s best to leave the turbo stream for more intricate use cases, and stick to default Rails controllers.

1 Like

I’d instead rely on the default Turbo behaviour - which is to do a replacement of the entire page body, i.e. not bother with turbo frames.

I’d like to do that too but it doesn’t replace the whole page on a redirect. I’ve tried target _top, I’ve tried status: 303, with a turbo_frame, without a turbo_frame, every trick I could find. It’s not replacing the whole page. It makes the redirect request and then just silently does nothing. I’ve explained a bit more here: Turbo new install - forms do not redirect · Issue #122 · hotwired/turbo-rails · GitHub

The only places in my app where I have Turbo still enabled for forms are places where a background job has to complete. The success case renders a success template which has a turbo_frame with an id of “new_resource” which replaces the contents of the form.

Either way I think it’s best to leave the turbo stream for more intricate use cases, and stick to default Rails controllers.

I’ve found the same thing. It’s best to have Turbo disabled for forms and only enable it on special cases, like the one I mentioned above.

Better late than never, but you can do something like this:
redirect_to whatever_path(format: :html)

1 Like

I’m seeing the same issue.

Server is sending a HTTP 303.

I can verify in the browser dev tools that the request is made to the new URL, and I can confirm in the response that the new request has a matching turbo-frame, but the browser URL remains with the pre-submit value, and it does not appear to be replacing the content of the turbo frame.

@andrewjmead I’ve found a solution, in this GH issue:

Which points to this fix:

Basically, as of that fix, you can add data-turbo-action="advance' to the form for which you want its HTTP 303 redirect URL to get “promoted” to be a visit. If your form targets another turbo frame, then that turbo frame must also include that data-turbo-action attribute too.

The docs are just not so helpful. :frowning:

1 Like

The default for non-matching turbo frame has now changed in 7.2 version of Turbo, and a navigation is “automatically” happening.

Change happened in this PR.

There’s also an event turbo:frame-missing that can be used for tighter control of the behaviour.

Let me know if you’re still having issues with turbo frames.

my team ran into this while upgrading to turbo-rails v1.4.0. We use turbo frames to (among other things) render modals which usually contain forms. We added the following event listener to application.js

document.addEventListener("turbo:frame-missing", (event) => {
  if (event.target.id === "modal") {
    event.preventDefault()
    event.detail.visit(event.detail.response.url)
  }
})

One interesting thing to note about turbo:frame-missing event is that it’s detail object has a visit method…