Skip to content

Commit

Permalink
Include minimal layout for frame responses (#428)
Browse files Browse the repository at this point in the history
Previously, responses to frame requests were rendered without any
layout. Since Turbo does not need the content outside of the frame,
reducing the amount that is rendered can be a useful optimisation.

However, this optimisation prevents responses from specifying
`head` content as well. There are cases where it would be useful for
Turbo to have access to items specified in head, like the meta tags used
for specifying visit and cache control.

To get around this, we change the optimisation to use a minimal layout
for frame responses, rather than no layout. With this, applications can
render content into the `head` if they want, and the Turbo Rails helpers
like `turbo_page_requires_reload` will also work.
  • Loading branch information
kevinmcconnell authored Feb 7, 2023
1 parent 6c8126a commit a2384ae
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 8 deletions.
22 changes: 15 additions & 7 deletions app/controllers/turbo/frames/frame_request.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
# Turbo frame requests are requests made from within a turbo frame with the intention of replacing the content of just
# that frame, not the whole page. They are automatically tagged as such by the Turbo Frame JavaScript, which adds a
# <tt>Turbo-Frame</tt> header to the request. When that header is detected by the controller, we ensure that any
# template layout is skipped (since we're only working on an in-page frame, thus can skip the weight of the layout), and
# that the etag for the page is changed (such that a cache for a layout-less request isn't served on a normal request
# and vice versa).
# <tt>Turbo-Frame</tt> header to the request.
#
# This is merely a rendering optimization. Everything would still work just fine if we rendered everything including the layout.
# Turbo Frames knows how to fish out the relevant frame regardless.
# When that header is detected by the controller, we substitute our own minimal layout in place of the
# application-supplied layout (since we're only working on an in-page frame, thus can skip the weight of the layout). We
# use a minimal layout, rather than avoid the layout entirely, so that it's still possible to render content into the
# <tt>head<tt>.
#
# Accordingly, we ensure that the etag for the page is changed, such that a cache for a minimal-layout request isn't
# served on a normal request and vice versa.
#
# This is merely a rendering optimization. Everything would still work just fine if we rendered everything including the
# full layout. Turbo Frames knows how to fish out the relevant frame regardless.
#
# The layout used is <tt>turbo_rails/frame.html.erb</tt>. If there's a need to customize this layout, an application can
# supply its own (such as <tt>app/views/layouts/turbo_rails/frame.html.erb</tt>) which will be used instead.
#
# This module is automatically included in <tt>ActionController::Base</tt>.
module Turbo::Frames::FrameRequest
extend ActiveSupport::Concern

included do
layout -> { false if turbo_frame_request? }
layout -> { "turbo_rails/frame" if turbo_frame_request? }
etag { :frame if turbo_frame_request? }
end

Expand Down
8 changes: 8 additions & 0 deletions app/views/layouts/turbo_rails/frame.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
4 changes: 4 additions & 0 deletions test/dummy/app/views/trays/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<% content_for :head do %>
<meta name="test" content="present" />
<% end %>

<turbo-frame id="tray">
<div>This is a tray!</div>
<div><%= @frame_id %></div>
Expand Down
27 changes: 26 additions & 1 deletion test/frames/frame_request_controller_test.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
require "test_helper"

class Turbo::FrameRequestControllerTest < ActionDispatch::IntegrationTest
test "frame requests are rendered without a layout" do
test "frame requests are rendered with a minimal layout" do
get tray_path(id: 1)
assert_select "title", count: 1

get tray_path(id: 1), headers: { "Turbo-Frame" => "true" }
assert_select "title", count: 0
end

test "frame request layout includes `head` content" do
get tray_path(id: 1), headers: { "Turbo-Frame" => "true" }

assert_select "head", count: 1
assert_select "meta[name=test][content=present]"
end

test "frame request layout can be overridden" do
with_prepended_view_path "test/frames/views" do
get tray_path(id: 1), headers: { "Turbo-Frame" => "true" }
end

assert_select "meta[name=test][content=present]"
assert_select "meta[name=alternative][content=present]"
end

test "frame requests get a unique etag" do
get tray_path(id: 1)
etag_without_frame = @response.headers["ETag"]
Expand All @@ -28,4 +44,13 @@ class Turbo::FrameRequestControllerTest < ActionDispatch::IntegrationTest
get tray_path(id: 1), headers: { "Turbo-Frame" => turbo_frame_request_id }
assert_match /#{turbo_frame_request_id}/, @response.body
end

private
def with_prepended_view_path(path, &block)
previous_view_paths = ApplicationController.view_paths
ApplicationController.prepend_view_path path
yield
ensure
ApplicationController.view_paths = previous_view_paths
end
end
9 changes: 9 additions & 0 deletions test/frames/views/layouts/turbo_rails/frame.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<meta name="alternative" content="present" />
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>

1 comment on commit a2384ae

@Aeramor
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates an issue with some settings. We get page change navigational issues unless we do render ... ,layout: false now.

Please sign in to comment.