hotwired / turbo-rails

Use Turbo in your Ruby on Rails app
https://turbo.hotwired.dev
MIT License
2.1k stars 324 forks source link

Turbo new install - forms do not redirect #122

Open kiddrew opened 3 years ago

kiddrew commented 3 years ago

I just installed Turbo in an existing Rails 6 app and all my existing forms are broken. They submit as expected but the redirect after doesn't happen. I'm able to interact with Turbo as expected - ie, I have a frame loading correctly, so it appears I loaded Turbo correctly in Webpack.

import "@hotwired/turbo-rails"

And with just a very simple form (using slim and simple_form):

= simple_form_for @comment do |f|
  = f.input :text, label: "Comment"
  = f.button :submit

My controller performs the redirect (using responders):

class CommentsController < ApplicationController
  load_and_authorize_resource

  def index
    @comment = Comment.new
  end

  def create
    @comment.save
    respond_with @comment, location: comments_path
  end

  private

  def resource_params
    params.require(:comment).permit(
      :text
    )
  end
end

The comment gets created and the request for the redirect URL happens but the redirect does not. I have to refresh to see the changes.

seanpdoyle commented 3 years ago

Thank you for reporting this.

Does your application run RailsUJS? Are your forms created with data-remote="true"?

kiddrew commented 3 years ago

I'm not using data-remote. I did have UJS enabled, but I disabled it to test and that didn't help either.

MohamedTaha123 commented 3 years ago

same issue here

SleeplessByte commented 3 years ago

I had to explicitly use render ..., status: :unprocessible_entity to make the page show with errors and redirect ..., status: :see_other to make the page redirect.

Perhaps that helps?

Petercopter commented 3 years ago

@SleeplessByte That is correct. It's not well documented yet, but Rails is switching to returning a 422 on fail: https://github.com/rails/rails/pull/41026

Devise will be ready for this change soon: https://github.com/heartcombo/devise/pull/5340

Really excited to start playing with these new toys 🎉

dixpac commented 3 years ago

@seanpdoyle there is a small issue with redirects. When using Rails UJS, and setting the form as local: false, data-turbo="false" redirects are not working.

respond_to do |format|
  format.html { redirect ...} <---- This will not redirect. Request is a js.erb, worked on turbolinks
end
seanpdoyle commented 3 years ago

@dixpac If RailsUJS is present and local: false is passed to form_with, the resulting form will have [data-remote="true"], and will be intercepted by RailsUJS ahead of Turbo, and will be submitted as a JS request instead of an HTML request.

Am I understanding your question properly?

kiddrew commented 3 years ago

I tried modifying the response to use :unprocessable_entity and :see_other and it didn't fix my issue. The submit and subsequent get request both work as expected but the form render or redirect doesn't happen.

dixpac commented 3 years ago

@seanpdoyle the problem is when migrating larger apps there is a lot of complex rails UJS code, which is impossible to migrate on turbo_streams because UJS is more powerful DOM manipulations wise.

So to make it work some forms need to be remote with data-turbo=false. Disabling the turbo enables old UJS code to continue working. But, trurbo-rails gem is missing this piece from the turbolinks, hence redirects are not working from the JS request.

This is maybe a different issue from the one @kiddrew posted.

seanpdoyle commented 3 years ago

@dixpac thsnks for clarifying. Let's open a different issue then.

As part of the new issue, could you please help us understand why we'd want to set local: true and [data-turbo="false"], instead of [data-turbo="true"] as part of a gradual migration?

dixpac commented 3 years ago

@seanpdoyle sure, I will open issue or a PR but I need more time to inspect what is happening 😄

As part of the new issue, could you please help us understand why we'd want to set local: true and [data-turbo="false"], instead of [data-turbo="true"] as part of a gradual migration?

It is local: false (remote: true) and data-turbo=false

seanpdoyle commented 3 years ago

@dixpac right, is data-turbo="false" preventing Turbo support, whereas data-turbo="true" (or omitting it entirely) might opt them back in?

dixpac commented 3 years ago

Yes. Basically I need to disable turbo on the form and let Rails UJS handle the form. That works like a charm, rails UJS handles properly until I redirect inside the controller, that redirect is "broken" because turbo-rails doesn't have that "magic" concern that was included in turbolinks (trubolinks gem is removed). I will try to execute Turbo.visit manually inside the .js.erb I think that could work.

This works on turblinks and doesn't work with turbo(remote: true, data-turbo=false):

# Form is remote: true
def create
  respond_to do |format|
    if person.save
       format.html { redirect .... } # redirect
    else
      format.js # Do some DOM manipulations with js
    end
  end
end
seanpdoyle commented 3 years ago

Thanks @dixpac. Could you open a separate issue?

uurcank commented 3 years ago

@seanpdoyle same issue here. Turbo always handles the form even the form has data-turbo="false" format.html ignored

  if @post.save
        format.turbo_stream 
        format.html { redirect_to posts_path, notice: Post was successfully created.'}
        format.json { render :show, status: :created, location: @post}
      end
jasonfb commented 3 years ago

@Petercopter --- I see https://github.com/heartcombo/devise/pull/5340 seems to fix the problem

I have steps to reproduce for Rails 6.1.3 and Devise 4.7.3 here:

https://stackoverflow.com/questions/66615478/turbo-rails-with-devise-does-not-redirect-consistently-rails-6-1-3-devise-4-7-3 My reproduction app against Devise 4.7.3 can be found here: https://github.com/jasonfb/TR002

I'd love to be able to upgrade my Rails 6.1 app for Turbo-Rails today, but my upgrade is riddled with these Devise bugs-- mostly redirects that don't redirect in the browser but the action has happened on the backend.

I see this pull appears to fix everything (🎉 !), true? false? truthy? falsy?

in master soon? Should I wait a bit? I guess if it were in master I could point my gem to the master branch of devise? Any tips appreciated, even just a monkey patch to get me going until the fix is released.

I got all the other parts of my TR upgrade working so as you can imagine I am eager to fix this last bit. Would be happy to help if there's any way I can.

seanpdoyle commented 3 years ago

@jasonfb thank you for creating https://github.com/jasonfb/TR001 to help reproduce the issue. Would it be possible for you to alter the git history so that the changes that are tied directly to reproducing the bug behavior are their own commit? It's very difficult to read through a commit that has a majority of its changes generated through Devise installation tasks.

After quickly scanning through, I have some high level questions:

1) Are the form_for calls generating HTML with [data-remote="true"]? 2) Is the format.turbo_stream { redirect_to "/nowhere" } call intentionally redirecting, or is it to demonstrate something else? Typically, redirect_to is better called from within the format.html { } block, or in a controller response that omits the format blocks entirely. 3) Have you tried replacing the link_to "Log out" with a button_to instead?

jasonfb commented 3 years ago

@seanpdoyle -- the whole app is to reproduce the issue. Yes I did it quickly and just made 1 commit. it's basically not very much more than 1) the turbo-rails install, a 2) the devise install, and 3) a small bit of customization

Oh bootstrap is in there for no reason -- I can take that out if you want.

Are the form_for calls generating HTML with [data-remote="true"]?

it would appear not. Screen Shot 2021-03-13 at 11 28 12 AM

Is the format.turbo_stream { redirect_to "/nowhere" } call intentionally redirecting, or is it to demonstrate something else?

So sorry… let me re-do, that is irrelevant now. (it was me debugging in some other way). The bug reproduces on devise directly, on the devise controller (which is not this one-- this HelloController#anything action is irrelevant, sorry).

FYI..... I think may be a devise issue related to devise because it seems like they said over there this was fixed in a PR. I will try the other things you suggest.

I removed the unneeded code, the anything action was unrelated to this --- just ignore it.

https://github.com/jasonfb/TR001/commit/f141ceaf2a60af041f9f591935647f02bfa65f45

jasonfb commented 3 years ago

Have you tried replacing the link_to "Log out" with a button_to instead?

You are a genius (although, this is still a bug). First: the devise login partials come from the devise gem itself, so without overriding it, I cannot change the links inside of the devise partials.

Focusing on Symptom # 2 only (the Logout)--- your suggestion does indeed instantly restore the functionality.

which is interesting, and suggests to me there is a bug related to links on the Turbo side. That is, since simply changing from link_to to button_to fixed the problem with the same Devise backend controller, shouldn't a Turbo behave the same way with links as it does with buttons (seems like turbo's bug?)

As far as the rest of this, it seems like it should be addressed in devise (for one thing, if mods are to be made to the login logout form). it looks like they were discussing root cause of these issues back in January here https://github.com/heartcombo/devise/pull/5325

(TBH I did not debug that part of the stack.-- so I can only speak to the part of the stack that I debugged of course but I see the thing appears to all be related to 422 status codes in Rails responses or something around this area. )

it makes sense in the sense that links are supposed to be non-destructive and buttons should do destructive things but UJS has spoiled us with method: :delete on our links.

So...... at the very least maybe Turbo could give a console error ?

jasonfb commented 3 years ago

Here you go ... new example app here:

https://github.com/jasonfb/TR002

this is now without boostrap and without haml, so just enough to reproduce the Turbo-rails + Devise issue

TR002 — fsevent_watch ◂ localhost:3000)  TR002  NVM_INC=:Users:jason: nvm:versions:node:v12 8 0:include:node rvm_bin_path=:User… 2021-03-13 15-08-25

I have cross-poseted this to https://github.com/heartcombo/devise/issues/5358

jasonfb commented 3 years ago

you can reproduce fully on your own machine using these steps https://gist.github.com/jasonfb/eb9cf8e90514dad1af0b98e01e9bce3d

dmitry-rychkov commented 3 years ago

Hello everyone,

I opened #152 earlier, but I think this issue is the same. For me it is completely not correct that Turbo prevents plain HTML forms from working with this gem: <form action="/url" method="POST">...</form>

It is old good HTML 1.0 and it gets broken unless you wrap it into<turbo-frame> and/or make explicit turbo_stream response! For me it doesn't matter what status code Rails return (though correct codes are a good practice), it is just broken HTML standard. Thus Turbo doesn't do progressive enhancement, it forces you into yet another ecosystem, like React, Angular and other vendor-lock tools.

This is not correct. Forms must work the same way as links. We don't need to opt-out from turbo on every link, so we shouldn't do it for every form. Links are smart enough to use whole page as response when not wrapped in turbo_frame and so should forms.

I suggest the following logic:

  1. Form tells server that it is capable of accepting turbo response (already done)
  2. On server if turbo_stream response is not defined, then use HTML response (TBD)
  3. Response may contain Turbo stream data-only or whole page, whatever - just like links work (already done for hyperlinks, for forms it works only if it is wrapped in frame, otherwise empty response is returned for some reason)
  4. After getting a response, client either redirects or renders HTML with the same logic as links do: if request was done from turbo-frame then replace frame. Otherwise replace whole HTML page. (already done for hyperlinks)

We will get everything working for all existing and all non-Rails code out of the box. It will respect old good HTML basics. And it will be progressive enhancement, not a mandatory Turbo lock-in. It will be effortless magic!

I think it is a more high-level problem of current implementation which will solve original redirect issue, too. It looks more server-side.

Sorry if I'm missing some big idea or technical restriction, but this part is confusing.

henrik commented 3 years ago

To add to what @dmitry-rychkov said, this would also improve the Turbolinks-to-Turbo upgrade path quite a lot.

Most of it was straightforward, but having form submissions suddenly doing nothing (even without remote: true and even with status: 303) was quite a head-scratcher. The docs also seem to say that you can progressively use just Turbo Drive and later opt into more fanciness with Frames/Streams, but reality currently seems to be that you can't use just Turbo Drive on its own for form submissions.

jasonfb commented 3 years ago

dmitry-rychkov commented 3 years ago

@jasonfb, my observation is that when server response doesn’t contain any turbo-frame, then it responds with empty response even if you generate full HTML page.

When there is a turbo-frame in response HTML then whole HTML page is returned and turbo works fine.

I couldn’t find a line in Rails code which prevents normal HTML response when there is no frame inside.

Still don’t see any reason for Turbo forms not to allow same logic as links (without any UJS)

gharmonjr commented 3 years ago

I ended up adding config.action_view.form_with_generates_remote_forms = false to my application.rb which prevented me needing to touch all forms by default

givemetraffic commented 3 years ago

Here is a quick fix for those who came there:

  1. Set config.action_view.form_with_generates_remote_forms = true at your initializer e.g. config/initializers/new_framework_defaults_6_1.rb
  2. Create a concern at app/controllers/concerns/turbo/redirection.rb

    module Turbo
    module Redirection
    extend ActiveSupport::Concern
    
    def redirect_to(url = {}, options = {})
      turbo = options.delete(:turbo)
    
      super.tap do
        visit_location_with_turbo(location, turbo) if turbo != false && request.xhr? && !request.get?
      end
    end
    
    private
    
    def visit_location_with_turbo(location, action)
      visit_options = {
        action: action.to_s == 'advance' ? action : 'replace'
      }
    
      script = []
      script << 'Turbo.clearCache()'
      script << "Turbo.visit(#{location.to_json}, #{visit_options.to_json})"
    
      self.status = 200
      self.response_body = script.join("\n")
      response.content_type = 'text/javascript'
      response.headers['X-Xhr-Redirect'] = location
    end
    end
    end

It's just a copy of the redirection handler from turbolinks-rails gem

  1. Add Turbo to the window at your application.js
    import { Turbo } from "@hotwired/turbo-rails"
    ...
    window.Turbo = Turbo
  2. Include concern at your ApplicationController
    class ApplicationController < ActionController::Base  
    include Turbo::Redirection
    ...

That's it. Your legacy code works.

lanzhiheng commented 3 years ago

@givemetraffic Thanks for your reply. I can find the similar code from Turbolink。 It works for me in my activeadmin's legacy code.

olieidel commented 3 years ago

For what it's worth, the solution in issue 138 in hotwired/turbo worked for me - simply setting data-turbo-frame = "_top" in the form. Full credit goes to @inopinatus.

That enables your form to accept responses which don't contain a turbo-frame and properly render the redirect.

morgler commented 3 years ago

Is there a solution for this yet?

I added Turbo to my Rails app and no form is working. I can confirm they're NOT remote (I even kicked rails-ujs, but it didn't help). As others I can confirm that redirects are being triggered and sent back to the browser, but the page in the browser remains frozen. Tricks like local=true or data-turbo-frame='_top' didn't do anything.

dhh commented 3 years ago

Here's the step we did for Basecamp 3 to arrive at coexistence for an app built for Turbolinks/UJS: https://world.hey.com/dhh/bringing-hotwire-to-basecamp-91a442d6

morgler commented 3 years ago

Thanks. Followed your instructions at https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md and tried so many ways to make it work - it still won't render the redirects :(. Only solution for me is to opt out of Turbo for all my forms using data-turbo='false'. Hoping for a fix in the future, because I like the lazy loading and the general idea.

kiddrew commented 3 years ago

I just tried again to upgrade from Turbolinks to Turbo using the concern method that @givemetraffic posted, and all my forms are still broken. Rails version is 6.1.4. Turbo-rails is 0.5.12. Manually adding data-turbo: false to each form works.

Edit: I noticed that button_to and simple_form_for don't use form_with, so the action_view config line doesn't make those remote automatically. I confirmed that forms built with form_with work as expected. Every form I have is generated using simple_form or button_to, so manually updating each appears to be my only option.

jclusso commented 3 years ago

I'm running into issues with this too. We have a download button that uses Rails UJS and submits a POST request to the action which redirects to an external URL for download using status: :see_other that I no longer can figure out how to get working.

kirylrb commented 3 years ago

It's a bit sad that the previous API is broken. This will be a huge stone on the road of migration for community.

SleeplessByte commented 3 years ago

@jclusso if you don't have a responds ... do |format| block, then Rails will respond with a "turbo stream" content-type which won't work. You can solve this by either adding a block like that, or explicitly setting content_type on the redirect.

schristm commented 2 years ago

Not being able to redirect the user to a new URL after form submission is making it difficult to adopt Turbo. Would it be reasonable to redirect the URL (Turbo.visit) if we either included, or even omitted, a specific status code? For example, if we redirect with a :status of :see_other, then Turbo will handle redirects as it does now, but if we used :found (or no status code) the browser would be redirected entirely.

SleeplessByte commented 2 years ago

IMO it it's absolutely a mistake to default to a turbo response but then ignoring :found. A server should not responds with a turbo response of :found if it knows that turbo won't be able to render it (it won't).

So instead, having rails respond with text/html (if :found) by default, would solve all these issues.

archonic commented 2 years ago

I got rid of Rails UJS since I was told it would conflict with Turbo and wrapped the form in a turbo_frame_tag. Displaying form errors works but the browser following the redirect isn't. The request format is turbostream and the response is in a format.html block. Like everyone else says, the browser is left hanging on the old page with the submitted form.

The really confusing thing to me is that the server renders the redirected page, which means the browser made the request for the redirect but didn't render it. I setup a bunch of logging on all the Turbo events and found that there's no render event. Investigating event.detail.fetchResponse.response for turbo:submit-end it seems to be perfectly aware that the client should redirect, it just doesn't.

Response {type: "basic", url: "http://lvh.me:3000/documents/72/labels", redirected: true, status: 200, ok: true, …}
body: (...)
bodyUsed: true
headers: Headers {}
ok: true
redirected: true
status: 200
statusText: "OK"
type: "basic"
url: "http://lvh.me:3000/documents/72/labels"
__proto__: Response

I wrote this SO question and got some insight. Changing format.html { redirect_to whatever_path } to format.html { redirect_to whatever_path(format: :html) } does actually change the behaviour despite the common format block. It went from hanging on the previous page with the submitted form to replacing the contents of the turbo_frame with nothing and the console complained about: Response has no matching <turbo-frame id="new_label"> element. I guess it's passing the format along as turbostream to the redirect despite entering the format.html block. The fallback to handling turbostream requests with html responses seems to put us in a weird place when upgrading.

The handbook says:

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.

That just doesn't seem to be happening. It's following the redirect, making the request with the turbostream content_type, then not rendering the result.

boardfish commented 2 years ago

Forcing the response to be considered HTML is a quick fix for this issue, but it results in some messy code. I'm having to do things like this:

    result = Command::Record::Create.new.call
    if result.failure?
      respond_to do |format|
        format.html do
          render View::Record::New.new(record: result.failure), status: :unprocessable_entity
        end
      end
      return
    end

    redirect_to comments_url(**comment_to_params(result.value!), format: :html), status: :see_other
jakemumu commented 2 years ago

After updating to Turbo I'm seeing this as well -- form a successfully submitting, but not redirecting -- this is when: as JS is a part of the Started POST -- is there a simple solution here? -- everything I'm seeing in this thread seems quite complex. -- if not, what's the way to handle a normal form?

SleeplessByte commented 2 years ago

(Almost) everything in this thread works.

archonic commented 2 years ago

@jakemumu After fighting the issue for a while I just disabled Turbo on all forms that have a potential redirect response. This PR fires an event (turbo:frame-missing) and makes it so Turbo.visit can accept response HTML, meaning you won't have to request a redirect URL twice.

(Almost) everything in this thread works.

Some of the things in this thread may work circumstantially. If you have a form in a turbo-frame, forcing the format to be HTML changes the behaviour but does not force it to render the redirect. Changing the status to 303 doesn't change anything. Using target _top may work but destroys the point of putting the form in a turbo-frame, which handles rendering form errors really well.

SleeplessByte commented 2 years ago

@archonic you've changed more things such as dropping Rails UJS and using a turbo frame tag, whereas we haven't. Ensuring you return HTML and responding with a 303 (or 307/308 -> 303 chain) work together with turbo. Using data-turbo="false" also works. When you still have UJS, making sure the forms aren't trying to submit via UJS and turbo is also mandatory. All that advice came from this thread.

I still think bad decisions were made regarding how turbo works, but that doesn't change that the advice does work.

2rba commented 2 years ago

as a workaround I have disabled turbo_stream at server created config/initializers/turbo.rb with

Rails.application.config.after_initialize do
  Mime::Type.unregister(:turbo_stream)
end
boardfish commented 2 years ago

Does that stop you from manually sending a turbo stream response?

vinaysolanki commented 2 years ago

A bit ridiculous that you can create forms now but can't redirect after submission. Is there a way to get rid of this JS crap completely and do simple html redirects? I'm stuck now right now. I've removed turbo-rails gem from Gemfile and the forms still don't redirect. What am I doing wrong?

archonic commented 2 years ago

If you disable turbo, it will be summited in the typical fashion. You can disable turbo on the form instead of removing it completely.

https://turbo.hotwired.dev/handbook/drive#disabling-turbo-drive-on-specific-links-or-forms

vinaysolanki commented 2 years ago

If you disable turbo, it will be summited in the typical fashion. You can disable turbo on the form instead of removing it completely. https://turbo.hotwired.dev/handbook/drive#disabling-turbo-drive-on-specific-links-or-forms

Thank you. That worked!

tomu123 commented 2 years ago

more than a year later and redirection still doesn't work with form when using turbo by default in rails 7, anyone got a solution?