Aaron Brager
Aaron Brager

Reputation: 66242

Rails 7 instance variable not passed to controller

On a new vanilla Rails 7 project, I'm trying to create a simple view with a form that, when submitted, renders the same view with some additional information.

Here's my controller:

class MyController < ApplicationController
  def index
    puts "got index request"
    @message = "type something into the form"
  end

  def create
    puts "got create request"
    @message = "hello world"
    render :index
  end
end

Here's index.html.erb:

<p style="color: green"><%= notice %></p>

<h1>New search</h1>

<%= render "form" %>

<p>Message is <%= @message.inspect %>.</p>

I expected the view to say Message is "hello world". since the instance variable should've been overwritten. However, it still says Message is "type something into the form"..

I tried 3 variants of render and got the same result:

I can see from the server logs that the create method is getting called on submit (it prints got create request) and it is rendering the index view (i see index.html.erb in the log); it's just that the instance variable isn't getting passed through.

Why isn't the instance variable passed to the view? Is something getting cached? What should I do instead?

Upvotes: 1

Views: 561

Answers (1)

Alex
Alex

Reputation: 29821

While it's technically explained here:
https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission

Sometimes it's hard to connect all the dots in Turbo.

# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    @message = "type something into the form"
  end

  def create
    @message = params[:form_message]
    render :index
  end
end

# app/views/messages/index.html.erb
<%= form_with url: "/messages" do |f| %>
  <%= f.text_field :form_message %>
  <%= f.submit %>
<% end %>
<div id="message_id"><%= @message %></div>

If you submit this form you get this in the browser console:

turbo.es2017-esm.js:2348 Error: Form responses must redirect to another location
    at O.requestSucceededWithResponse (turbo.es2017-esm.js:780:27)
    at I.receive (turbo.es2017-esm.js:543:27)
    at I.perform (turbo.es2017-esm.js:518:31)

status: :unprocessable_entity

After submitting a form, you have to redirect or render an error response, default is status 422 aka :unprocessable_entity:

render :index, status: :unprocessable_entity

render turbo_stream:

Because forms are submitted as TURBO_STREAM format, you can render turbo_stream response:

respond_to do |format|
  format.html { render :index, status: 422 }
  format.turbo_stream do
    render turbo_stream: turbo_stream.update(:message_id, params[:form_message])
  end #    ^       helper^      action^      ^id          ^content
end   # response format

This is probably the preferred way.


turbo_frame

Put it in a frame. Not as efficient as turbo_stream, because it renders the whole page on the server, but on the front end effect is the same, only message is updated:

<%= form_with url: "/messages", data: { turbo_frame: :message_id } do |f| %>
  <%= f.text_field :form_message %>
  <%= f.submit %>
<% end %>

<%= turbo_frame_tag :message_id do %>
  <%= @message %>
<% end %>

Just render :index works fine.


method: :get

Send GET request instead:

# app/views/messages/index.html.erb
<%= form_with url: "/messages", method: :get do |f| %>
...

# app/controllers/messages_controller.rb
def index
  @message = params[:form_message] || "type something into the form"
end

override turbo render

Because the server is actually rendering a response and sending it back, we can override turbo rendering logic and just render the response instead of complaining:

// app/javascript/application.js

document.addEventListener("turbo:before-fetch-response", async (event) => {
  if (event.target.hasAttribute("data-render-post")) {
    event.preventDefault()
    const response = new DOMParser().parseFromString(await event.detail.fetchResponse.responseHTML, "text/html")
    // you're free, do what you like here
    document.body.replaceWith(response.body)
  }
})
# app/views/messages/index.html.erb

# i imagine you don't want to do that for every form
#                                       vvvvvvvvvvvvvvvvv
<%= form_with url: "/messages", data: { render_post: true } do |f| %>
...

redirect_to

The usual redirect then render will work. You can also have turbo_stream response in redirected action. Or turbo_frame_tag in the template if form is targeting a turbo_frame:

def create
  redirect_to message_path(1, form_message: params[:form_message])
end

def show
  @message = params[:form_message]
  respond_to do |format|
    format.html
    format.turbo_stream { render turbo_stream: turbo_stream.update(:message_id, @message) }
  end
end

And it behaves the same as described above, there is just an extra redirect in between.

Upvotes: 2

Related Questions