How to have a delete link respond to turbo_stream and html in Rails 7?
Asked Answered
D

1

5

I have a delete link in Rails 7 that functions correctly using either turbo_stream or html, but not each of those.

link_to 'delete', @object, data: { turbo_method: 'delete', turbo_confirm: 'Really?' }

I call this link from the index page, which should use turbo_stream response to delete the record and remove the table row. The index page is wrapped in a turbo-frame tag. I also call this method from the show page, where an html response should delete the record and redirect back to the index page. The show page is not wrapped in a turbo-frame tag.

The show page link correctly hits the destroy action and destroys the record---however, it does not redirect. It actually responds to turbo_stream. If I remove the format.turbo_stream block from the destroy action, then that same link correctly hits the format.html response and redirects. That same link knows how to respond to format.html, but it instead attempts to respond to format.turbo_stream even though the link was not wrapped in a turbo-frame tag.

In Rails 7, the data attribute "turbo_method: 'delete'" results in a turbo_stream call. Is there a way to tell that link to respond to format.html?

How can I get the link on the show page to respond to format.html and redirect--when the incoming response from the link is turbo_stream?

Direction answered 6/3, 2023 at 21:44 Comment(0)
K
18

I have mentioned headers a few times recently, so I'll keep this part short:

When you send a TURBO_STREAM request, the first format that takes priority is turbo_stream. If you don't have a turbo_stream format block or a turbo_stream.erb template, then html format is used. Because turbo can handle both of these responses, it sets both types in Accept header, which determines what format block to run. You can take a look at it from destroy action:

puts request.headers["Accept"]
#=> text/vnd.turbo-stream.html, text/html, application/xhtml+xml
#   ^                           ^
#   turbo is first in line      html is second

def destroy
  @model.destroy

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.remove(@model) }
    format.html { redirect_to models_url, notice: "Destroyed." }
  end
end

To get a turbo_stream response

<%= link_to "Turbo destroy", model_path(model),
  data: {turbo_method: :delete}
%>

<%= button_to "Turbo destroy", model_path(model),
  method: :delete
%>

To get an html response

Rails can also ignore Accept header and determine the format from a url extension. Turbo request to /models/1.html will respond with html.

<%= link_to "HTML turbo destroy", model_path(model, format: :html),
  data: {turbo_method: :delete}
%>

<%= button_to "HTML turbo destroy", model_path(model, format: :html),
  method: :delete
%>

# using a form field also works
# like `hidden_field_tag :format, :html` inside your form:
<%= button_to "HTML turbo destroy with format input", model_path(model),
  method: :delete,
  params: {format: :html}
%>

My least favorite option turbo: false, yuck:

<%= button_to "HTML rails destroy", model_path(model),
  method: :delete,
  data: {turbo: false}
%>

Use url or form params to do whatever you want

<%= button_to "Turbo destroy with params", model_path(model),
  method: :delete,
  params: {redirect_to: "/anywhere/you/like"} # or maybe just true/false
%>
def destroy
  @model.destroy

  respond_to do |format|
    # just pass a param and skip turbo_stream block
    unless params[:redirect_to]
      format.turbo_stream { render turbo_stream: turbo_stream.remove(@model) }
    end
    format.html { redirect_to (params[:redirect_to] || models_url), notice: "Destroyed." }
  end
end

You can also set the format explicitly:

# it doesn't have to be a callback, just has to happen before `respond_to` block.

before_action :guess_destroy_format, only: :destroy

def guess_destroy_format
  # this way you don't need `unless params[:redirect_to]` around turbo_stream
  request.format = :html if params[:redirect_to]

  # don't need to do anything extra if deleting from a show page
  request.format = :html if request.referrer.start_with?(request.url)
end

https://api.rubyonrails.org/classes/ActionDispatch/Http/MimeNegotiation.html


To get any response with Accept header

Maybe you need to hide that ugly .html or you don't want to mess with controllers to much. Set Accept header and get just what you need. Note that Turbo will handle html and turbo_stream, but you'll have to handle any other responses yourself:

// app/javascript/application.js

const Mime = {
  turbo_stream: "text/vnd.turbo-stream.html",
  html:         "text/html",
  json:         "application/json",
}

document.addEventListener('turbo:submit-start', function (event) {
  const {
    detail: {
      formSubmission: {
        fetchRequest: { headers },
        submitter: { dataset: { accept } },
      },
    },
  } = event

  if (Mime[accept]) {
    headers["Accept"] = Mime[accept]
  }
})

Use data-accept to set the type:

<%= button_to "only html",  model, method: :delete,
  data: {accept: :html}
%>

<%= button_to "only turbo", model, method: :delete,
  data: {accept: :turbo_stream}
%>
Keare answered 7/3, 2023 at 0:48 Comment(2)
Wow, Alex! What a great answer and explanation. That helped explain a lot. :)Direction
I studied one thing: you can use turbo_steeam.remove, this method not appear in the book: Agile web development with rails 7, it shows me just one: turbo_stream.replace.Steam

© 2022 - 2024 — McMap. All rights reserved.