I’m using turbo-rails library with Rails 7.0. I have a page that shows a list of users in a group and a form for adding a new user by email. The form is inside a turbo frame. When form is submitted there are two choices:
If the email is incorrect it will show the form with a validation error “User not found” by rendering the form partial only and replacing the turbo-frame’s content.
Otherwise, if the user is found, it will add the user to the group and re-render the entire page using the Rails redirect_to function.
#1 works, but #2 does not. Rails responds with the redirect but the page is not updated (i.e. I don’t see a new user added to the list). How do I make both #1 and #2 work? Is there a better approach to achieve what I want? Is there a parameter for redirect_to method to break out of the turbo frame?
My implementation
The form for adding a new user is inside a Rails partial _add_user_form.html.erb:
The view groups/users.html.erb renders the partial and the list of users in the group:
<h1>Users</h1>
<!-- renders the form from _add_user_form.html.erb partial -->
<p>User 1</p>
<p>User 2</p>
<p>User 3</p>
In the Rails controller groups_controler.rb I have
def add_user
user = User.find_by(email: params[:email])
if user.nil?
return render(
partial: 'add_user_form',
locals: {
email: params[:email],
user_not_found: true
}
)
end
group = Group.find(params[:id])
group.users << user # Add the user to the group
redirect_to users_group_path(group)
end
Solution that sucks
Of course, I can get rid of the turbo frame, and re-render the entire page when form is submitted (with validation errors or not). But I hoped to use the turbo frame functionality, so I only render the form on validation error and keep the rest of the page unchanged.
There was a similar question posted here that i forgot the link to it. But, basically you needed to use a Stimulus Controller to listen for the events.
When the form has validation errors, i.e status: :unprocessable_entity it will do nothing. However, if the response status is success, it visits the ur.
// form_redirect_controller.js
import { Controller } from "@hotwired/stimulus"
import * as Turbo from "@hotwired/turbo"
export default class extends Controller {
next(event) {
if (event.detail.success) {
const fetchResponse = event.detail.fetchResponse
history.pushState(
{ turbo_frame_history: true },
"",
fetchResponse.response.url
)
Turbo.visit(fetchResponse.response.url)
}
}
}
Thanks @rockwell! This is an excellent workaround, since I can write this Stimulus controller once and reuse it in my forms. Having said that, to me this solution feels a bit … hacky. I wish there was a built-in way to conditionally break out of the turbo frame programmatically, from a Rails controller for example.
If it were me, I’d drop the turbo-frame and use turbo-stream with http instead. No additional javascript needed.
def add_user
@user = User.find_by(email: params[:email])
@group = Group.find(params[:id])
if user.nil?
respond_to do |format|
format.turbo_stream
format.html { redirect_to users_group_path(group) }
end
else
group.users << user # Add the user to the group
redirect_to users_group_path(group)
end
end
do you mean to NOT have <turbo-frame id='add_user_form'> in the html at all? If so, how will turbo_stream.update 'add_user_form' work? Doesn’t it reference the ID of the turbo frame to update?
Sorry, I should’ve added an id the form. The turbo-stream will look for the DOM element with the ID, and the perform the action (replace, update, delete, etc).