Redirect after Turbo stream response

Hello. I was wondering if there’s a way to perform a redirect after returning a Turbo stream response in Rails.

For instance, in the chat app from the Hotwire screencast, they say that one can use a Stimulus controller to clear the message form field, but if we have form errors we would have to clear those too; the new message frame kind of keeps unmodified, without a render or redirection.

The following code from the chat app makes it think that it could return either a stream or a redirect when successfully saving the message model, but not both:

respond_to do |format|
  if @message.save
    format.turbo_stream # maybe something here to respond with a render/redirect too?
    format.html { redirect_to @room }
  else
    format.html { render :new, status: :unprocessable_entity }
  end
end

One use case I can think of is to clear the form and its errors by redirecting the frame to the same path (this would avoid the need to create Stimulus controllers to clear form fields/errors). Another use case is to render a “New comment” button which replaces its frame to the comment’s form when clicked, and redirect back to the new comment button when the form submission is successful (apart from using Turbo streams to broadcast the new comment and rendering it in another frame, like the chat app example).

My workaround to this is to save the redirect path as a data attribute in the DOM and create a Stimulus controller to redirect using Turbo.visit, like so:

import { Controller } from "stimulus";
import { Turbo } from "@hotwired/turbo-rails";

export default class extends Controller {
  formRedirect(event) {
    const redirectPath = this.element.dataset.redirect;

    if (event.detail.success) {
      Turbo.visit(redirectPath);
    }
  }
}

But what I would want is not to rely on custom JavaScript code, and for Turbo[-rails] to be able to return both a stream and render/redirect.

2 Likes

I think I just found the solution. It seems that it’s as simple as redirecting inside the turbo_stream block:

format.turbo_stream { redirect_to article_path(@article) }

Maybe I just did something wrong before because I tried it and it didn’t quite work.

I just need to confirm if this is the desired behaviour.

1 Like

I’ve had a similar use case. A flash message (turbo_stream.erb) needs to be briefly shown inside a modal. Afterward, the page has to be redirected.

A solution for me was to add a data-controller=“redirect” and set the URL value on the wrapping div from flash message. The path is being passed as a local from the turbo_stream.erb file.

flash.html.erb

<div data-controller="redirect" data-redirect-url-value="<%= path %>">
  <%= message %>
</div>

redirect_controller.js

import { Controller } from 'stimulus'

export default class extends Controller {
  static values = { url: String }
  connect () {
    window.location.href = this.urlValue
  }
}
3 Likes

It works(loads the whole page of the path we provided), thanks @thomasvanholder . But not sure how do we load only the turbo frame of the path we provided.

1 Like

@Tenzin_Dorjee Agreed! That reloads the whole page. But it would be great to make it just go back to the previous frame.

Here is a working demonstration of the solution suggested by @thomasvanholder :

Todo details (show) redirects to the index page when a todo is marked completed. Even if it happens in the backend or another browser window.

Thank you @thomasvanholder for the solution. It worked without any problems. However, can anyone tell me why do we need the line below to work?

  static values = { url: String }

Also this one as well… (I can guess but where can I read more about this convention?) Thanks

data-redirect-url-value="<%= path %>"

Hey I think you’re looking for the docs on Stimulus Values: Stimulus Reference

Hope that helps!

The solution while retarded is pretty simple to this problem.

For a turbo_stream.erb response, you want the flash.now[:alert] way but for a redirect you need to set the flash[:alert] before redirecting. See the below code for a working example:

  def create
    authorize! :create, current_company.sell_orders.new

    @sell_order = current_company.sell_orders.new(sell_order_params.compact_blank)
    @out_car    = current_company.cars.includes(:branch).find(params[:out_car_id])

    if params[:trade]
      trade_regnr = trade_params[:regnr].upcase
      if (trade = Car.find_or_create_by_regnr(trade_regnr))
        trade.update!(trade_params)
      else
        respond_to do |format|
          format.turbo_stream do
            flash.now[:alert] = t("flash.trade_car_not_found", regnr: trade_regnr)
            render :new, status: :unprocessable_entity, alert: alert_message
          end
        end
      end 
    end

    if @sell_order.save
      respond_to do |format|
        format.turbo_stream do
          flash[:success] = t("flash.sell_order_created")
          redirect_to [:edit, @sell_order], status: :see_other
        end
      end
    else
      respond_to do |format|
        format.turbo_stream do
          flash.now[:alert] = t("flash.actions.create.alert", resource_name: "Säljorder")
          render :new, status: :unprocessable_entity
        end
      end
    end
  end