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?
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
<%= link_to "Turbo destroy", model_path(model),
data: {turbo_method: :delete}
%>
<%= button_to "Turbo destroy", model_path(model),
method: :delete
%>
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}
%>
<%= 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
Accept
headerMaybe 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}
%>