ruby-on-railshotwire-railsturboturbo-frameshotwire

How to break out of a turbo frame for redirect on a form submit


I am trying to dynamically render form errors using a turbo-frame. This works great when there is an error and correctly to populates the form with the submitted params, but when the form submit is successful the redirect doesn't work.

Here is the code I tried:


app/views/orders/show.html.erb

...
<%= render "order_review_form", locals: {order: @order} %>
...

app/views/_order_review_form.html.erb

<%= turbo_frame_tag "order_review_form" do %>
  <% if local_assigns[:error] %>
    <p class="alert alert-danger alert-dismissible fade show">
      <%= sanitize(local_assigns[:error]) %>
      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    </p>
  <% end %>

  <%= form_with(model: Review.new, url: order_review_path(local_assigns[:order])) do |f| %>
    <div>
      <%= f.label :comment %>
      <%= f.text_field :comment, value: params[:comment] %>
    </div>

    <%= submit_tag "Submit review", class: "btn btn-sm btn-success" %>
  <% end %>
<% end %>

app/controllers/order_reviews_controller.rb

...
def create
  OrderReviewCreator.call(@order, params)
  redirect_to(order_path(@order), status: :see_other) # this doesn't work, the page doesn't update
rescue AppError => e
  render(
    "_order_review_form",
    locals: {
      order: @order,
      error: e.detail,
    },
    status: :unprocessable_entity,
  )
end
...


Solution

  • The solution was to use a turbo stream response in the controller instead of a turbo frame.


    app/views/orders/show.html.erb (no changes)

    ...
    <%= render "order_review_form", locals: {order: @order} %>
    ...
    

    app/views/shared/_form_error.html.erb

    Notes:

    <div id="<%= target %>">
      <% if local_assigns[:error] %>
        <p class="alert alert-danger alert-dismissible fade show">
          <%= sanitize(local_assigns[:error]) %>
          <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </p>
      <% end %>
    </div>
    

    app/views/_order_review_form.html.erb

    Changes:

    <%= render(partial: "shared/form_error", locals: { target: OrderReviewsController::FORM_ERROR_TARGET }) %>
    
    <%= form_with(model: Review.new, url: order_review_path(local_assigns[:order])) do |f| %>
      <div>
        <%= f.label :comment %>
        <%= f.text_field :comment%>
      </div>
    
      <%= submit_tag "Submit review", class: "btn btn-sm btn-success" %>
    <% end %>
    

    app/controllers/order_reviews_controller.rb

    Changes: Added the constant FORM_ERROR_TARGET so it can be referenced in the view and the controller and changed the render to render a turbo stream for the form error. Note that the FORM_ERROR_TARGET is passed in the render in two places. This effectively replaces whatever html element has the id of FORM_ERROR_TARGET with the contents of the partial.

    ...
    FORM_ERROR_TARGET = "order-review-form-error"
    ...
    def create
      OrderReviewCreator.call(@order, params)
      redirect_to(order_path(@order), status: :see_other)
    rescue AppError => e
      render(
        turbo_stream: turbo_stream.replace(
          FORM_ERROR_TARGET,
          partial: "shared/form_error",
          locals: {
            target: FORM_ERROR_TARGET,
            error: e.detail,
          },
        ),
        status: :unprocessable_entity,
      )
    end
    ...