๐Ÿ›  [How-To] Form with redirection on success with hotwired/turbo

Snippets and explanation for a basic behaviour with forms when turbo is enabled

November 20, 2021 - 4 minute read -
EN Ruby on Rails

This article is basically a snippet for a common pattern in web apps, and I struggle to find the right way each time I want to implement this behaviour in my rails apps with turbo enabled ๐Ÿ˜…

What we want

On a form submission, I want to redirect to a new page on success, and render errors otherwise.

Letโ€™s take a dummy model for our example: a Post with title/description as text attributes

Without turbo

In plain old HTTP without js/javascript, the controller is pretty straightforward:

class PostsController < ApplicationController
  def new
    @post = Post.new
  end

  def create
    @post = Post.new(params.require(:post).permit(:title, :description))

    if @post.save
      redirect_to posts_path
    else
      render :new
    end
  end
end

And our new.html.erb view:

<% if @post.errors %>
  <ul>
    <% @post.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

<%= form_for(@post) do |f| %>
  <%= f.text_field :title %>
  <%= f.text_field :description %>
  <%= f.submit 'Send' %>
<% end %>

When turbo is enabled, it does not work at all.

FYI, If you want to disable turbo you can add the data-turbo="false" to the form (which solved the issue).

With turbo

Like I said in the intro, I struggle each time to have a working version of this with turbo, which consists of a dynamic display of errors and a simple redirect to my path.

The process:

Wrap your form (with errors) in a turbo-frame tag, and add a turbo-frame="_top" on the form:

<turbo-frame id="new_post">
  <% if @post.errors %>
    <ul>
      <% @post.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  <% end %>

  <%= form_for(@post, data: { turbo_frame: '_top' }) do |f| %>
    <%= f.text_field :title %>
    <%= f.text_field :description %>
    <%= f.submit 'Send' %>
  <% end %>
</turbo-frame>

The tricky part here is the turbo-frame="_top": without it turbo intercept the redirection, and does not perform a โ€œfullโ€ reload of our page.

In the controller, on success use http status 303 (See Other), and on failure use a 422 http status (Unprocessable entity)

class PostsController < ApplicationController
  def new
    @post = Post.new
  end

  def create
    @post = Post.new(params.require(:post).permit(:title, :description))

    if @post.save
      redirect_to posts_path,
        status: :see_other
    else
      render :new,
        status: :unprocessable_entity
    end
  end
end

Turbo relies a lot on HTTP status. When the server renders a 4xx or 5xx error, it does not perform the redirection.

At this point, it does not render errors yet. Thanks to this issue (and this comment), I managed to find a way to solve this.

All you need to do is to write a new.turbo_stream.erb which weโ€™ll be triggered by the render :new in our controller:

<%= turbo_stream.replace("new_post", partial: 'new' %>

When you submit your form, it accepts turbo stream as response, adding a view with turbo stream format bypass entirely the html response and let us to do what we want: render the form (and the errors) on our frame.

Note: You have to put your entire โ€˜newโ€™ view in a partial (or just your form) in order to be able to use a partial here.

At this point, it should be OK.