-
Notifications
You must be signed in to change notification settings - Fork 430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to override frame-target from server response #257
Comments
I got some use cases where controlling that behavior from the server would be useful. Session expiration is one of them. |
I like this idea and think it is inline with the programming model turbo is built for, which is letting the server do the work. |
How is this problem currently being solved? I'm at this exact situation and not sure what I can do to work around it. |
I'd be happy to see something like this. |
The bandwidth benefits of optimizing `Turbo-Frame:` header responses to omit the layout do not offset the HTTP-layer cache busting trade-offs. Similarly, there's an opportunity to implement behavior changes related to: * [@hotwired/turbohotwired#361][] * [@hotwired/turbohotwired#257][] if both `<turbo-frame>` elements' `FrameController` instances and `Session` instances were able to share `Visit` instances built from fully formed `<html>` pages. By trimming the layout and outer portions of the document, `turbo-rails` forces frames to deal with incomplete fragments. [@hotwired/turbohotwired#257]: hotwired/turbo#257 [@hotwired/turbohotwired#361]: hotwired/turbo#361
The bandwidth benefits of optimizing `Turbo-Frame:` header responses to omit the layout do not offset the HTTP-layer cache busting trade-offs. Similarly, there's an opportunity to implement behavior changes related to: * [@hotwired/turbohotwired#361][] * [@hotwired/turbohotwired#257][] if both `<turbo-frame>` elements' `FrameController` instances and `Session` instances were able to share `Visit` instances built from fully formed `<html>` pages. By trimming the layout and outer portions of the document, `turbo-rails` forces frames to deal with incomplete fragments. [@hotwired/turbohotwired#257]: hotwired/turbo#257 [@hotwired/turbohotwired#361]: hotwired/turbo#361
Unfortunately, sending a response with A fetch According to the 2.2.6. Responses section of the specification:
The Atomic HTTP redirect handling section of the specification states:
I'm no CORS expert, but the specification mentions some CORS-related leeway with regard to headers. Unless I'm missing a crucial concept, I don't think there is a way for Turbo to excise the server's There are some related discussions on
AlternativesIf those limitations hold, we'll need to investigate alternatives to sending back a header.
I've ranked them from least to most regrettable. I'm hoping I'm missing something obvious here! |
I've had success with a work-around that writes to the Rails session and sends down a # app/controllers/concerns/turbo_frame_redirectable.rb
module TurboFrameRedirectable
extend ActiveSupport::Concern
included do
before_action -> { session[:turbo_frames] ||= {} }, if: -> { turbo_frame_request? }
after_action :write_turbo_frame_header, if: -> { turbo_frame_request? && request.get? }
end
# Override Rails' default redirect_to so that it accepts a `turbo_frame:` option
#
# redirect_to post_url(@post), status: :see_other, turbo_frame: "_top"
#
def redirect_to(*arguments, **options, &block)
if (turbo_frame = request.headers["Turbo-Frame"]) && options.key?(:turbo_frame)
session[:turbo_frames].reverse_merge! turbo_frame => options[:turbo_frame]
end
super
end
private
def write_turbo_frame_header
response.headers["Turbo-Frame"] = session[:turbo_frames].delete request.headers["Turbo-Frame"]
end
end Then, read that value on the client-side and intervene when import { clearCache, visit } from "@hotwired/turbo"
addEventListener("turbo:submit-start", ({ detail: { formSubmission: requestFormSubmission } }) => {
const { formElement: form, fetchRequest: request } = requestFormSubmission
if (request.headers["Turbo-Frame"]) {
const listenForTurboFrameOverride = ({ detail: { fetchResponse: response, formSubmission: responseFormSubmission } }) => {
if (responseFormSubmission === requestFormSubmission
&& response.redirected
&& response.header("Turbo-Frame") == "_top") {
clearCache()
visit(response.location)
}
}
form.addEventListener("turbo:submit-end", listenForTurboFrameOverride, { once: true })
}
}) This combination of workarounds is functional, but has its drawnbacks:
While it's still unclear how servers will ensure that a Whenever a response with a I've opened #397 to experiment with that. |
hotwired#257 --- While it's still unclear how servers will persist a `Turbo-Frame:` header override across destructive actions and the `GET` requests that result from their `303 See Other` responses, the client side concern is more straightforward: Whenever a response with a `Turbo-Frame: _top` header is handled by a `FrameController`, propose a full-page Visit. This commit adds test coverage for both `<a>` element initiated `GET` requests, as well as `<form>` element initiated `POST` requests.
hotwired#257 --- While it's still unclear how servers will persist a `Turbo-Frame:` header override across destructive actions and the `GET` requests that result from their `303 See Other` responses, the client side concern is more straightforward: Whenever a response with a `Turbo-Frame: _top` header is handled by a `FrameController`, propose a full-page Visit. This commit adds test coverage for both `<a>` element initiated `GET` requests, as well as `<form>` element initiated `POST` requests.
The bandwidth benefits of optimizing `Turbo-Frame:` header responses to omit the layout do not offset the HTTP-layer cache busting trade-offs. Similarly, there's an opportunity to implement behavior changes related to: * [@hotwired/turbohotwired#361][] * [@hotwired/turbohotwired#257][] if both `<turbo-frame>` elements' `FrameController` instances and `Session` instances were able to share `Visit` instances built from fully formed `<html>` pages. By trimming the layout and outer portions of the document, `turbo-rails` forces frames to deal with incomplete fragments. [@hotwired/turbohotwired#257]: hotwired/turbo#257 [@hotwired/turbohotwired#361]: hotwired/turbo#361
hotwired#257 --- While it's still unclear how servers will persist a `Turbo-Frame:` header override across destructive actions and the `GET` requests that result from their `303 See Other` responses, the client side concern is more straightforward: Whenever a response with a `Turbo-Frame: _top` header is handled by a `FrameController`, propose a full-page Visit. This commit adds test coverage for both `<a>` element initiated `GET` requests, as well as `<form>` element initiated `POST` requests.
hotwired#257 --- While it's still unclear how servers will persist a `Turbo-Frame:` header override across destructive actions and the `GET` requests that result from their `303 See Other` responses, the client side concern is more straightforward: Whenever a response with a `Turbo-Frame: _top` header is handled by a `FrameController`, propose a full-page Visit. This commit adds test coverage for both `<a>` element initiated `GET` requests, as well as `<form>` element initiated `POST` requests.
Why do we need to return "_top" at all? What if when a response from the server does not include header['turbo-frame'], couldn't Turbo just assume _top? and treat the response as a standard turbo request (updates the page URL, swaps the entire body, etc)? I know at the moment if the response to a turbo-frame request does not include a matching turbo-frame ID, then nothing happens on the page and an error is logged into the console. Is there any reason we couldn't make this the default behavior? Is there a scenario I'm not thinking of? This solve multiple scenarios that we've run into, including a common scenario where a turbo-frame request responds with a session timeout redirect to the login page. |
I think it could be the case for _top and wanting a full page redirect. However being able to return and change the frame in the server response has other benefits in being able to conditionally replace frames server side too. |
@tleish that's an interesting suggestion!
The only determining factor is the presence or absence of a Requiring that Turbo detect a If this feature is deemed worthwhile, it might be worth the breaking changes. Am I understanding your proposal correctly? |
@seanpdoyle - yes, this was my original thinking, but I misunderstood the logic for how turbo-frame determined to log an error on a missing turbo-frame response. Does turbo-frame look for the actual ID in the response DOM? Where ever the existing logic is to replace the current turbo-frame or log an error, instead of just logging the error of missing turbo-frame ID I'm suggesting it also load the page/URL. The logic would be located in the same location, it would not be breaking (for Rails or any other server-side frameworks). In what situation would the software receive an invalid turbo-frame response that you would want to do nothing? Situations I can think of missing turbo-frame ID response:
|
@tleish I do think that handling a response without a matching Having said that, the code mentioned in #257 (comment) was developer with slightly different situations in mind. For example, when there is a matching |
@seanpdoyle - I personally prefer keeping the breakout login in the HTML response vs a header. Most Common ScenariosThe most common reasons for breaking out of a frame involve no edge case changes to code:
I feel like developer should not need to add additional code to break out of turbo-frames in the above common scenarios (e.g. add Client Side Determines Turbo-Frame BreakWith Turbo today, if a developer wants the server response to be rendered inside the turbo-frame, they must customize the HTML response to include the turbo-frame id (make sense). Today, they can also define breaking out of turbo-frame in the HTML (even if response has matching turbo-frame#id) <turbo-frame id="messages" target="_top">
<a href="/messages/1">
Following link will replace the whole page, even if response has matching turbo-frame#id.
</a>
<form action="/messages">
Submitting form will replace the whole page, even if response has matching turbo-frame#id.
</form>
</turbo-frame> or <turbo-frame id="messages">
<a href="/messages/1" data-turbo-frame="_top">
Following link will replace the whole page, even if response has matching turbo-frame#id.
</a>
<form action="/messages" data-turbo-frame="_top">
Submitting form will replace the whole page, even if response has matching turbo-frame#id.
</form>
</turbo-frame> Server Side Determines Turbo-Frame BreakIf a developer is creating custom code to include a matching turbo-frame, but they do not want the client to use the matching turbo-frame, then it make sense to me that they also add custom code to handle this case. I could see this as one of two options:
In option 1
In option 2
Client <turbo-frame id="messages">
<a href="/messages/1">Message 1</a>
</turbo-frame> Server Response <turbo-frame id="<%= 'message-1' unless turbo_frame_target_top? %>">
<a href="/messages/1">Message 1</a>
</turbo-frame> The above says I don't want this HTML to be included in an existing turbo-frame if a certain condition is met. <turbo-frame id="<%= @turbo_frame_id %>">
<a href="/messages/1">Message 1</a>
</turbo-frame> For me, I prefer option 2. |
I think there are two scenarios worth highlighting and distinguishing from one To help guide the discussion around them, let's focus on a concrete use case: A multi-step form within a
|
@seanpdoyle - that's an interesting scenario. 1. A dynamic turbo-frame IDOne approach is to create a unique id for the dialog frame and pass the frame as an ID <body>
- <turbo-frame id="dialog_frame">
+ <%= dialog_frame_id = params[:dialog_frame_id] || "dialog_frame_#{Time.now.to_i}" %>
+ <turbo-frame id="<%= dialog_frame_id %>">
<h1>My Title</h1>
<form method="post" action="/articles">
+ <input hidden="dialog_frame_id" value="<%= dialog_frame_id %>">
<input type="hidden" name="article[title"] value="My Title">
<label for="article_body">Body</label>
<textarea id="article_body" name="article[body]"></textarea>
<button>Submit</button>
</form>
</turbo-frame>
</body> The downside of this approach is the complexity that I now have to update all targets which reference the frame on the current page and responding page. It also feels kludgy, but may be worth considering depending on how "corner case" you consider the scenario . 2. Respond with a headerHave the server respond with a Turbo-Frame header. The downside is this is far from the turbo-frame, so troubleshooting why the behavior is altered could prove difficult. BTW, does Turbo include other features a developer can change by responding with specific header? 3. Respond with a
|
Something else to consider with any of these scenarios, what happens if the response indicates to break out of the frame but it does not have a In other words: turbo-rails/app/controllers/turbo/frames/frame_request.rb layout -> { false if turbo_frame_request? } |
I agree! This is part of the reasoning behind hotwired/turbo-rails#232. I haven't pushed as hard for that because I'm not sure if we've exhausted other options. Having said that, I believe that communicating in fully-formed HTML documents has other benefits. |
Implementing with Turbo StreamsIt's worth mentioning that something like this is currently possible with a Consider the example HTML from before: <html>
<head><!-- ... --></head>
<body>
<main>
<form action="/articles/new" data-turbo-frame="dialog_frame">
<button aria-expanded="false" aria-controls="dialog">New article</button>
</form>
</main>
<dialog id="dialog" data-controller="dialog" data-action="turbo:frame-load->dialog#showModal">
<turbo-frame id="dialog_frame"></turbo-frame>
</dialog>
</body>
</html> Instead of trying to navigate the page in compliance with the frame, let's add <html>
<head><!-- ... --></head>
<body>
<main>
<form action="/articles/new" data-turbo-frame="dialog_frame">
<button aria-expanded="false" aria-controls="dialog">New article</button>
</form>
</main>
<dialog id="dialog" data-controller="dialog" data-action="turbo:frame-load->dialog#showModal">
- <turbo-frame id="dialog_frame"></turbo-frame>
+ <turbo-frame id="dialog_frame" target="_top"></turbo-frame>
</dialog>
</body>
</html> Let's consider a hypothetical Rails controller's implementation. We'll render As a side-effect of handling multiple steps in the same controller, the code is 🚨Pseudo-code ahead 🚨: class Article < ApplicationRecord
validates :title, presence: true, on: :title
with_options presence: true do
validates :title
validates :body
end
end
class ArticlesController < ApplicationController
def new
@article = Article.new title: params[:title]
end
def create
@article = Article.new article_params
case params[:step]
when "title"
if @article.validate context: :title
params[:step] = "body"
render :new
else
render :new, status: :unprocessable_entity
end
when "body"
if @article.save
redirect_to article_url(@article)
else
render :new, status: :unprocessable_entity
end
else
render :new, status: :unprocessable_entity
end
end
def show
@article = Article.find params[:id]
end
private def article_params
params.require(:article).permit(:title, :body)
end
end In tandem with a controller like that, consider an <%# app/views/articles/_step.html.erb %>
<% case local_assigns[:step] when "title" %>
<h1>What's the title?</h1>
<form method="post" action="/articles">
<label for="article_title">Title</label>
<input id="article_title" name="article[title]" value="<%= @article.title %>">
<button>Next</button>
</form>
<% when "body" %>
<h1><%= @article.title %></h1>
<form method="post" action="/articles">
<input type="hidden" name="article[title"] value="<%= @article.title %>">
<label for="article_body">Body</label>
<textarea id="article_body" name="article[body]"><%= @article.body %></textarea>
<button>Submit</button>
</form>
<% else %>
<h1>Writing an Article</h1>
<a href="/help">Learn more</a>
<a href="/articles/new?step=title" data-turbo-frame="dialog_frame">Get started</a>
<% end %> <%# app/views/articles/new.html.erb %>
<turbo-frame id="dialog_frame">
<%= render partial: "articles/step", locals: { article: @article, step: params[:step] } %>
</turbo-frame> <%# app/views/articles/new.turbo_stream.erb %>
<turbo-stream target="dialog_frame" action="update">
<template>
<%= render partial: "articles/step", locals: { article: @article, step: params[:step] } %>
</template>
</turbo-stream> In this case, the controller is capable of responding to typical HTML requests, Making it work with Turbo Frames and redirectsPersonally, I have a strong distaste for responding to I'm still very interested in supporting a multi-step form Thank you, @tleish. I appreciate these suggestions. Some of them are new to me Whichever server-side detail we'd check for while "breaking out" during a That means that a A query parameter might work, but "breaking out" based on I hope I'm misunderstanding the constraints of the |
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][]. [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
Thanks for this issue and i hope you can find a way. Its a show stopper for many cases. i currently built a turbo app. Turbo is so cool but when you really want to bring it to shine the most point is always the responding of the create action of the controller. At this point its not a nice-to have, its a must that you have to decide inside the controller whether responding as I now made a workaround by stimulus. It works but its not a clean soulution and a little bit cumbersome, or too cumbersome for apps where you want to save time. |
Hi, can you elaborate on what the work around was?
…On Sat, Jan 14, 2023 at 11:00 PM Christian ***@***.***> wrote:
Thanks for this issue and i hope you can find a way.
Its a show stopper for many cases.
i currently built a turbo app. Turbo is so cool but when you really want
to bring it to shine the most point is always the responding of the create
action of the controller. At this point its not a nice-to have, its a must
that you have to decide inside the controller whether responding as
turbo-stream or, in case of success: by a simple redirect_to.
I now made a workaround by stimulus. It works but its not a clean
soulution and a little bit cumbersome, or too cumbersome for apps where you
want to save time.
—
Reply to this email directly, view it on GitHub
<#257 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABXR7XFEVTIXGLV2EJKSRDWSKIOHANCNFSM43N5SCPQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
I think we can close this since latest versions of Turbo now allow creating custom actions. So can easily create a redirect_to action which will do a Turbo.visit. I think redirect_to should be standard action, however this is how I now solve all these conditional streams use cases. |
@acetinick i could find nothing in the official docs @saltysealion |
@chmich You can use https://github.com/marcoroth/turbo_power-rails which provides a bunch of custom actions including redirect |
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][], and marking it with [data-turbo-cache="false"][] [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript [data-turbo-cache="false"]: https://turbo.hotwired.dev/reference/attributes#data-attributes
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][], and marking it with [data-turbo-cache="false"][] [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript [data-turbo-cache="false"]: https://turbo.hotwired.dev/reference/attributes#data-attributes
I had similar problem and have found a way to fix it easily. My idea: When i need to show a [bootstrap] modal with some form or wizard, i want to load it from the server, and if after submit it does not pass validation, updated version should be reloaded, not affecting the whole page. So how i approached it initially: I have added an empty The form inside modal also has What happens: Because my whole page template includes placeholder for modal, it updates only that placeholder. How i solved it: I wrapped actual modal with additional turbo-frame. So now, form submissions update only that additional frame (which looks absolutely the same for the end user), by when i make a full redirect, that inner turbo-frame is not present in the response, and this causes turbo to render full page. |
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][], and marking it with [data-turbo-cache="false"][] [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript [data-turbo-cache="false"]: https://turbo.hotwired.dev/reference/attributes#data-attributes
Hello @elik-ru, I have exactly your same scenario and I'm stuck at Unfortunately I can't understand how you solved the problem by adding a nother turbo frame. Could you please elaborate? What do you mean with Thanks a lot! |
@lukepass That how it worked for me:
Bad news is that it's not working anymore. That behavior was changed here: #863 (and included in version 7.3.0). Now instead of doing full page update it shows "Content missing" inside the frame (modal_inner in my case) |
@lukepass But it looks like you can restore this functionality by adding a custom event handler to
event.target contains "turbo-frame#modal_inner", so you can check it if needed. |
In the meantime, I wrote the render_turbo_stream gem. There are some helpers that make it easier to control the turbo actions in general. |
I have a very similar use-case as @elik-ru, and #863 made this an issue that that is once again very relevant. The inability to specify from the server whether a response should be a full page load causes a lot of problems when dealing with handling failure cases for a form submission when inside a modal, since only the server knows whether the page should redirect or render an error, not the client. It seems there are 2 options at the moment, neither of which is ideal as an end user:
|
If someone is interested, 204 (no content) status can be used given how rare it is. To jump out of frame, I use this (Symfony): class TurboRedirectResponse extends Response
{
public function __construct(string $url)
{
parent::__construct(status: 204, headers: ['location' => $url]);
}
} The important bit, event listener. All these checks for nullability are because I don't want to risk failures and I am terrible with JS: document.addEventListener('turbo:submit-end', (event) => {
let response = event.detail?.fetchResponse?.response;
let status = response?.status;
let url = response?.headers?.get('Location') ?? null;
if (status === 204 && url) {
Turbo.visit(url, {action: 'advance' }) // this should also have `frame` value, not tested yet
event.preventDefault();
return false;
}
}) And finally controller: public function doSomething(): Response
{
if ($shouldControllerRedirect) {
return new TurboRedirectResponse($url); // should have target frame as second param here
}
return new Response('<turbo-frame id="test">This is where form is rendered as usual</turbo-frame>');
} The idea is far from perfect and right now, issues a full visit but that is only because of missing |
I took me some time to understand how to make the workaround work perfectly. First you must understand one thing about layouts. It you don't specify layout in your controller - turbo-rails does it for you with this piece of code:
So when doing frame requests it renders special tiny layout, which saves render time and transferred bytes, which is what we want. But if for some reason you need custom layout and specify it as
Now we have custom full layout by default, and tiny layout for turbo-frame requests. Nice. Next we come to the break-out problem. That proposed solution kinda worked for me, but not ideally:
It is detected correctly, but Turbo is making full page reload, doing 2 requests in a row. I was digging around, tried to do So here is the final solution:
And same js snippet:
So, what we are doing here?
Finally, Last question: what happens if some redirect occurs without our flash message? Well, it still will be working, with that double load and full-page visit, but still working, so I think it's a good fallback for unexpected cases. |
I was able to override the frame-target using the meta tag. It's explained here in the docs:
|
@krschacht It will make a full-page reload. Meaning 2 requests for the page, and reloading/reevaluating of JS/CSS. |
One relatively common subcase of this problem is when you want to break out of the frame and redirect the full page back to itself to refresh it. Since the introduction of morphing we also have the refresh stream action which will always apply itself to the full page. So, that case can be solved relatively simply by having the controller emit the refresh action when the submission is successful: render turbo_stream: turbo_stream.action(:refresh, "") (Not yet a built in action, pending hotwired/turbo-rails#595). I agree with the sentiments some people expressed here that it doesn't feel right to use stream actions as response to a form submission but at the moment that is probably the simplest, most maintainable, solution. |
Closes hotwired/turbo#257 Closes hotwired/turbo#397 Follow-up to: * hotwired/turbo#257 (comment) * hotwired/turbo#257 (comment) Depends on hotwired/turbo#660 Introduces the `Turbo::Stream::Redirect` concern to override the [redirect_to][] routing helper. When called with a `turbo_frame:` option, the `redirect_to` helper with check whether the request was made with the Turbo Stream `Accept:` header. When it's absent, the response will redirect with a typical HTTP status code and location. When present, the controller will respond with a `<turbo-stream>` element that invokes [Turbo.visit($URL, { frame: $TURBO_FRAME })][hotwired/turbo#649] where `$URL` is set to the redirect's path or URL, and `$TURBO_FRAME` is set to the `turbo_frame:` argument. This enables server-side actions to navigate the entire page with a `turbo_frame: "_top"` option. Incidentally, it also enables a frame request to navigate _a different_ frame. Typically, an HTTP that would result in a redirect nets two requests: the first submission, then the subsequent GET request to follow the redirect. In the case of a "break out", the same number of requests are made: the first submission, then the subsequent GET made by the `Turbo.visit` call. Once the `Turbo.visit` call is made, the script removes its ancestor `<script>` by calling [document.currentScript.remove()][], and marking it with [data-turbo-cache="false"][] [redirect_to]: https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to [hotwired/turbo#649]: hotwired/turbo#649 [document.currentScript.remove()]: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript [data-turbo-cache="false"]: https://turbo.hotwired.dev/reference/attributes#data-attributes
Currently if the response from turbo requests returns a redirect response whilst inside a frame, there is no way for the server side to conditionally control what turbo to do with the response, eg. force a full page refresh with target="_top"
There are use cases especailly for modal windows where upon succesful save we would want to perform a redirect back to the index page, where as 422 response would do the normal turbo frame replacement within current frame.
It would be nice to support something like this, which will allow us to control targets from serverside to frame targets by setting frame target via the http response headers.
What everyones thoughts are on this? or is there something I am missing to make this easier.
The text was updated successfully, but these errors were encountered: