Skip to content

Commit

Permalink
Add root layouts (#3679)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismccord authored and josevalim committed Mar 9, 2020
1 parent 3bd5720 commit a81dd86
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 12 deletions.
82 changes: 71 additions & 11 deletions lib/phoenix/controller.ex
Expand Up @@ -488,25 +488,25 @@ defmodule Phoenix.Controller do
@spec put_layout(Plug.Conn.t, {atom, binary | atom} | atom | binary | false) :: Plug.Conn.t
def put_layout(%Plug.Conn{state: state} = conn, layout) do
if state in @unsent do
do_put_layout(conn, layout)
do_put_layout(conn, :phoenix_layout, layout)
else
raise AlreadySentError
end
end

defp do_put_layout(conn, false) do
put_private(conn, :phoenix_layout, false)
defp do_put_layout(conn, private_key, false) do
put_private(conn, private_key, false)
end

defp do_put_layout(conn, {mod, layout}) when is_atom(mod) do
put_private(conn, :phoenix_layout, {mod, layout})
defp do_put_layout(conn, private_key, {mod, layout}) when is_atom(mod) do
put_private(conn, private_key, {mod, layout})
end

defp do_put_layout(conn, layout) when is_binary(layout) or is_atom(layout) do
defp do_put_layout(conn, private_key, layout) when is_binary(layout) or is_atom(layout) do
update_in conn.private, fn private ->
case Map.get(private, :phoenix_layout, false) do
{mod, _} -> Map.put(private, :phoenix_layout, {mod, layout})
false -> raise "cannot use put_layout/2 with atom/binary when layout is false, use a tuple instead"
case Map.get(private, private_key, false) do
{mod, _} -> Map.put(private, private_key, {mod, layout})
false -> raise "cannot use put_layout/2 or put_root_layout/2 with atom/binary when layout is false, use a tuple instead"
end
end
end
Expand All @@ -526,6 +526,47 @@ defmodule Phoenix.Controller do
end
end

@doc """
Stores the root layout for rendering.
Like `put_layout/2`, the layout must be a tuple,
specifying the layout view and the layout name, or false.
In case a previous layout is set, `put_root_layout` also
accepts the layout name to be given as a string or as an atom. If a
string, it must contain the format. Passing an atom means the layout
format will be found at rendering time, similar to the template in
`render/3`. It can also be set to `false`. In this case, no layout
would be used.
## Examples
iex> root_layout(conn)
false
iex> conn = put_root_layout conn, {AppView, "root.html"}
iex> root_layout(conn)
{AppView, "root.html"}
iex> conn = put_root_layout conn, "bare.html"
iex> root_layout(conn)
{AppView, "bare.html"}
iex> conn = put_root_layout conn, :bare
iex> root_layout(conn)
{AppView, :bare}
Raises `Plug.Conn.AlreadySentError` if `conn` is already sent.
"""
@spec put_root_layout(Plug.Conn.t, {atom, binary | atom} | atom | binary | false) :: Plug.Conn.t
def put_root_layout(%Plug.Conn{state: state} = conn, layout) do
if state in @unsent do
do_put_layout(conn, :phoenix_root_layout, layout)
else
raise AlreadySentError
end
end

@doc """
Sets which formats have a layout when rendering.
Expand Down Expand Up @@ -562,6 +603,12 @@ defmodule Phoenix.Controller do
@spec layout(Plug.Conn.t) :: {atom, String.t | atom} | false
def layout(conn), do: conn.private |> Map.get(:phoenix_layout, false)

@doc """
Retrieves the current root layout.
"""
@spec root_layout(Plug.Conn.t) :: {atom, String.t | atom} | false
def root_layout(conn), do: conn.private |> Map.get(:phoenix_root_layout, false)

@doc """
Render the given template or the default template
specified by the current action with the given assigns.
Expand Down Expand Up @@ -726,7 +773,7 @@ defmodule Phoenix.Controller do
def __put_render__(conn, view, template, format, assigns) do
content_type = MIME.type(format)
conn = prepare_assigns(conn, assigns, template, format)
data = Phoenix.View.render_to_iodata(view, template, Map.put(conn.assigns, :conn, conn))
data = render_with_layouts(conn, view, template, format)

conn
|> ensure_resp_content_type(content_type)
Expand All @@ -735,7 +782,6 @@ defmodule Phoenix.Controller do

defp instrument_render_and_send(conn, format, template, assigns) do
template = template_name(template, format)

view =
Map.get(conn.private, :phoenix_view) ||
raise "a view module was not specified, set one with put_view/2"
Expand All @@ -750,6 +796,20 @@ defmodule Phoenix.Controller do
send_resp(conn)
end

defp render_with_layouts(conn, view, template, format) do
render_assigns = Map.put(conn.assigns, :conn, conn)

case root_layout(conn) do
{layout_mod, layout_tpl} ->
inner = Phoenix.View.render(view, template, render_assigns)
root_assigns = render_assigns |> Map.put(:inner_content, inner) |> Map.delete(:layout)
Phoenix.View.render_to_iodata(layout_mod, template_name(layout_tpl, format), root_assigns)

false ->
Phoenix.View.render_to_iodata(view, template, render_assigns)
end
end

defp prepare_assigns(conn, assigns, template, format) do
assigns = to_map(assigns)
layout =
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/views.exs
Expand Up @@ -8,6 +8,13 @@ end

defmodule MyApp.LayoutView do
use Phoenix.View, root: "test/fixtures/templates"
import Phoenix.HTML

def render("root.html", assigns) do
~E"""
ROOTSTART[<%= @title %>]<%= @inner_content %>ROOTEND
"""
end

def default_title do
"MyApp"
Expand Down
25 changes: 25 additions & 0 deletions test/phoenix/controller/controller_test.exs
Expand Up @@ -83,6 +83,31 @@ defmodule Phoenix.Controller.ControllerTest do
end
end

test "put_root_layout/2 and root_layout/1" do
conn = conn(:get, "/")
assert root_layout(conn) == false

conn = put_root_layout(conn, {AppView, "root.html"})
assert root_layout(conn) == {AppView, "root.html"}

conn = put_root_layout(conn, "bare.html")
assert root_layout(conn) == {AppView, "bare.html"}

conn = put_root_layout(conn, :print)
assert root_layout(conn) == {AppView, :print}

conn = put_root_layout(conn, false)
assert root_layout(conn) == false

assert_raise RuntimeError, fn ->
put_root_layout(conn, "print")
end

assert_raise Plug.Conn.AlreadySentError, fn ->
put_layout sent_conn(), {AppView, :print}
end
end

test "put_new_layout/2" do
conn = put_new_layout(conn(:get, "/"), false)
assert layout(conn) == false
Expand Down
24 changes: 23 additions & 1 deletion test/phoenix/controller/render_test.exs
Expand Up @@ -7,7 +7,7 @@ defmodule Phoenix.Controller.RenderTest do
import Phoenix.Controller

defp conn() do
conn(:get, "/") |> put_view(MyApp.UserView) |> fetch_query_params
conn(:get, "/") |> put_view(MyApp.UserView) |> fetch_query_params()
end

defp layout_conn() do
Expand Down Expand Up @@ -41,13 +41,35 @@ defmodule Phoenix.Controller.RenderTest do
assert html_response?(conn)
end

test "renders string template with put_root_layout" do
conn =
conn()
|> put_layout({MyApp.LayoutView, "app.html"})
|> put_root_layout({MyApp.LayoutView, "root.html"})
|> render("index.html", title: "Hello")

assert conn.resp_body == "ROOTSTART[Hello]<html>\n <title>Hello</title>\n Hello\n\n</html>\nROOTEND\n"
assert html_response?(conn)
end

test "renders atom template with put layout" do
conn = put_format(layout_conn(), "html")
conn = render(conn, :index, title: "Hello")
assert conn.resp_body =~ ~r"<title>Hello</title>"
assert html_response?(conn)
end

test "renders atom template with put_root_layout" do
conn =
conn()
|> put_layout({MyApp.LayoutView, "app.html"})
|> put_root_layout({MyApp.LayoutView, :root})
|> render("index.html", title: "Hello")

assert conn.resp_body == "ROOTSTART[Hello]<html>\n <title>Hello</title>\n Hello\n\n</html>\nROOTEND\n"
assert html_response?(conn)
end

test "renders template with overriding layout option" do
conn = render(layout_conn(), "index.html", title: "Hello", layout: false)
assert conn.resp_body == "Hello\n"
Expand Down

0 comments on commit a81dd86

Please sign in to comment.