-
Notifications
You must be signed in to change notification settings - Fork 347
Document how to add a layout for turbo frame requests #351
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
Document how to add a layout for turbo frame requests #351
Conversation
# Then, you have to create a layout with the corresponding name. It can be handy for flash messages, for example: | ||
# | ||
# # app/views/layouts/application.turbo_stream.erb | ||
# <%= turbo_stream.update('flash', partial: 'layouts/flash') %> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for proposing this documentation!
The PR's title and the preceding code sample mention Frames, while the layout code sample mentions the turbo_stream
helper and the template's file extension mentions turbo_stream.erb
.
As I understand them, Streams and Frames are unrelated (but complementary) tools.
I think the first paragraph could be related to #232 (comment), and feels appropriate in this file.
Is there another, more appropriate area of the codebase we could outline the Streams layout pattern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpdoyle, thank you very much for your feedback!
You are right, turbo frame requests and turbo stream requests are two different things. In fact, in most cases, we don't want the layout for turbo frame requests that are not turbo stream requests, as we are only going to extract and replace one single turbo frame.
However, we want to have this application.turbo_stream.erb
layout in the case of a turbo stream request for flash messages. Maybe we could add a method to identify turbo stream requests like this:
def turbo_stream_request?
request.accept.starts_with?(Mime[:turbo_stream])
end
Then we could only remove the layout for turbo frame requests that are not turbo stream requests:
layout -> { (turbo_frame_request? && !turbo_stream_request?) ? false : "application" }
Does it make sense? I just added a new commit so we can discuss this, let me know what you think! If we agree on this, maybe we can then improve the documentation even further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for clarifying @alexandreruban.
I was under the impression that a app/views/layouts/application.turbo_stream.erb
layout is already supported, without needing to override the layout from the controller. Has that changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpdoyle sorry for the late reply.
The application.turbo_stream.erb
layout is supported when we override the layout -> { false if turbo_frame_request? }
of the turbo-rails
gem with layout "application"
in our ApplicationController
for example.
However, this has an unintended side effect. Now, when making GET requests by clicking links inside a Turbo Frame, the view is rendered with the application.html.erb
layout. We don't need any layout in that case because Turbo is going to replace only the content of a specific Frame.
In fact, what we often want is to add an application.turbo_stream.erb
layout for Turbo Stream requests (for flash messages most of the time) but not when we make Turbo Frame requests that are not Turbo Stream requests.
I created a reproduction repository to clearly show what I mean. The description each commit is very precise and will show you precisely the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexandreruban thank you for sharing that reproduction repository.
I think I understand the issue now. I really appreciate the granularity of the commits, it helped me tell the difference between the code generated by Rails and the code that was pertinent to the issue.
# | ||
# # app/controllers/articles_controller.rb | ||
# class ApplicationController < ActionController::Base | ||
# layout -> { (turbo_frame_request? && !turbo_stream_request?) ? false : "application" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In most cases, Turbo will submit form requests with the Turbo Stream Accept:
header present, so turbo_stream_request?
would evaluate to true
, and any response would be rendered with the "application"
layout.
Unfortunately, whether or not a response uses a .html.erb
or .turbo_stream.erb
template hinges on the response, and not the request.
For example, consider a POST
request to /messages
with an Accept:
header containing the Turbo Stream MIME type, and a Turbo-Frame: my-frame
header. The presence of both headers means that:
turbo_frame_request?
evaluates totrue
turbo_stream_request?
evaluates totrue
!turbo_stream_request?
evaluates tofalse
turbo_frame_request? && !turbo_stream_request?
evaluates tofalse
That means that the layout
expression would return "application"
.
Consider a hypothetical messages#create
controller action that supports both HTML and Turbo Stream responses:
def create
@message = Message.new(messages_params)
if @message.save
respond_to do |format|
format.html { redirect_to @message, status: :see_other }
format.turbo_stream
end
else
render :new, status: :unprocessable_entity
end
end
In the case of the format.turbo_stream
block, calls to turbo_stream_request?
would return true
, and the documentation's guidance makes sense: render the messages/create.turbo_stream.erb
template into the app/views/layouts/application.turbo_stream.erb
layout.
However, since the POST
request's Accept:
header includes the Turbo Stream MIME type, calls to turbo_stream_request?
in the format.html
branch would also evaluate to true
. If the subsequent request that follows the redirect is made with the same header value (Turbo-Frame
and Accept
, for example) as the original POST
request, then calls to turbo_stream_request?
and turbo_frame_request?
within that request would also evaluate to true
. This means that no matter what, responses are rendered into an "application"
layout.
Now consider a messages#create
controller action that only responds with HTML:
def create
@message = Message.new(messages_params)
if @message.save
- respond_to do |format|
- format.html { redirect_to @message, status: :see_other }
- format.turbo_stream
- end
+ redirect_to @message
else
render :new, status: :unprocessable_entity
end
end
The absence of a respond_to call combined with the redirect_to @message
means that the response is going to have an HTML MIME type, not a Turbo Stream MIME type. In that scenario, the layout
block's inclusion of !turbo_stream_request?
would still cause the response body to render with the application
layout.
Personally, I'm in favor of always rendering responses into a layout, but that would require a broader discussion than this one.
Am I misunderstanding something about this process, or something about your particular use case that makes this scenario a non-issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again for your response. I agree with everything you said above, and you are right for the redirect_to @message
, I forgot to consider this case as I rarely use it.
The use case was consider a MessagesController#new
action:
def new
@message = Message.new
end
If the link to the new message page is in a Turbo Frame, we would have the following headers:
Accept: text/html, application/xhtml+xml
Turbo-Frame: new_article
This means that layout -> { (turbo_frame_request? && !turbo_stream_request?) ? false : "application" }
would evaluate to false
and so in that specific case, we wouldn't have a layout.
I will close the PR as it does not work in all cases. Thanks again for your review!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the answer is that we need a way at a top-level to set whether you want to use layouts for turbo frames (rendering them in layouts/application.turbo_frame.html) and then document how to overwrite that on a per-action basis with format.turbo_frame { render layout: "something_else" }
.
References #178
Adding a layout for
turbo frame
requests is a very common feature that is currently not documented. This PR documents how to add a layout forturbo frame
requests with the common example of flash messages.