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:
<turbo-frame id='add_user_form'>
<form action="/group/123/add_user" method="post">
<!-- some validation -->
<input type="email" name="email" />
<input type="submit" name="Add user" />
</form>
</turbo-frame>
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.
3 Likes
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)
}
}
}
Wiring it up with your html
<turbo-frame id='add_user_form'>
<form action="/group/123/add_user" method="post" data-controller='form-redirect' data-action='turbo:submit-end->form-redirect#next'>
<!-- some validation -->
<input type="email" name="email" />
<input type="submit" name="Add user" />
</form>
</turbo-frame>
3 Likes
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.
2 Likes
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
<!-- add_user.turbo_stream.erb -->
<%= turbo_stream.update 'add_user_form' %>
<form action="/group/123/add_user" method="post">
<!-- some validation -->
<input type="email" name="email" />
<input type="submit" name="Add user" />
</form>
<% end %>
2 Likes
Your solution looks very nice! When you say
I’d drop the turbo-frame
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).
Original Form:
<form action="/group/123/add_user" method="post" id="add_user_form">
<!-- some validation -->
<input type="email" name="email" />
<input type="submit" name="Add user" />
</form>
add_user.turbo_stream.erb using replace
<%= turbo_stream.replace 'add_user_form' %>
<form action="/group/123/add_user" method="post" id="add_user_form">
<!-- some validation -->
<input type="email" name="email" />
<input type="submit" name="Add user" />
</form>
<% end %>
or using update
<%= turbo_stream.update 'add_user_form' %>
<input type="email" name="email" />
<input type="submit" name="Add user" />
<% end %>
I recommend reading the documentation on how turbo-streams work.
1 Like
The turbo-stream will look for the DOM element with the ID
Oh so the first argument of turbo_stream.replace
method can be an ID of ANY element, not just limited to a turbo-frame. I did not know that, thanks!
I recommend reading the documentation on how turbo-streams work.
I will do that!
Totally genius. I’ve been looking for something like this for a while. Thanks for your post. I don’t see how this would be considered hacky. It uses Stimulus JS called from a callback built into Turbo. HOTWIRE is composed of Turbo, Stimulus JS and soon Strada. So combining Stimulus with Turbo is per the documentation.
1 Like
I’ve registered here just to thank you for that great point!
I’ve been having issues with turbo frames over and over. Last one is about the update that doesn’t allow to break out of the frame (Turbo Handbook). I.e. I submit the form in that frame, and after it’s successfully submitted I want to do redirect and reload the whole page, which became super complex to achieve with turbo frames.
So dropping turbo-frame and using just turbo-stream made my life so much easier and the code so much cleaner and stable. Yay!