Skip to content

Commit

Permalink
Support function components via component/3 (#1428)
Browse files Browse the repository at this point in the history
  • Loading branch information
msaraiva committed Apr 23, 2021
1 parent 6936ba8 commit 3356970
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 32 deletions.
2 changes: 2 additions & 0 deletions lib/phoenix_live_view/engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,8 @@ defmodule Phoenix.LiveView.Engine do

defp classify_taint(:live_component, [_, _, [do: _]]), do: :render
defp classify_taint(:live_component, [_, _, _, [do: _]]), do: :render
defp classify_taint(:component, [_, [do: _]]), do: :render
defp classify_taint(:component, [_, _, [do: _]]), do: :render
defp classify_taint(:render_layout, [_, _, _, [do: _]]), do: :render

defp classify_taint(:alias, [_]), do: :always
Expand Down
54 changes: 54 additions & 0 deletions lib/phoenix_live_view/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,45 @@ defmodule Phoenix.LiveView.Helpers do
end
end

@doc """
Renders a stateless component defined by a function.
Takes two optional arguments, assigns and a do block that will be used as
the @inner_block.
All of the `assigns` given are forwarded directly to the function as
the first only argument.
## Examples
The function can either local:
<%= component(&weather_component/1, city: "Kraków") %>
Or remote:
<%= component(&MyApp.Weather.component/1, city: "Kraków") %>
"""
defmacro component(func, assigns \\ [], do_block \\ []) do
{do_block, assigns} =
case {do_block, assigns} do
{[do: do_block], _} -> {do_block, assigns}
{_, [do: do_block]} -> {do_block, []}
{_, _} -> {nil, assigns}
end

{assigns, inner_block} = rewrite_do(do_block, assigns, __CALLER__)

quote do
Phoenix.LiveView.Helpers.__component__(
unquote(func),
unquote(assigns),
unquote(inner_block)
)
end
end

defp rewrite_do(nil, opts, _caller), do: {opts, nil}

defp rewrite_do([{:->, meta, _} | _] = do_block, opts, _caller) do
Expand Down Expand Up @@ -313,6 +352,7 @@ defmodule Phoenix.LiveView.Helpers do
assigns = if inner, do: Map.put(assigns, :inner_block, inner), else: assigns
id = assigns[:id]

# TODO: Deprecate stateless live component
if is_nil(id) and
(function_exported?(component, :handle_event, 3) or
function_exported?(component, :preload, 1)) do
Expand All @@ -328,6 +368,20 @@ defmodule Phoenix.LiveView.Helpers do
raise "expected #{inspect(module)} to be a component, but it is a #{kind}"
end

@doc false
def __component__(func, assigns, inner)
when is_function(func) and is_list(assigns) or is_map(assigns) do
assigns = Map.new(assigns)
assigns = if inner, do: Map.put(assigns, :inner_block, inner), else: assigns

func.(assigns)
end

def __component__(func, assigns)
when is_list(assigns) or is_map(assigns) do
raise ArgumentError, "component/3 expected an anonymous function, got: #{inspect(func)}"
end

@doc """
Renders the `@inner_block` assign of a component with the given `argument`.
Expand Down
15 changes: 15 additions & 0 deletions test/phoenix_live_view/controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,19 @@ defmodule Phoenix.LiveView.ControllerTest do
conn = get(conn, "/controller/live-render-4")
assert html_response(conn, 200) =~ "title: Dashboard"
end

test "renders function components from dead view", %{conn: conn} do
conn = get(conn, "/controller/render-with-function-component")
assert html_response(conn, 200) =~ "RENDER:COMPONENT:from component"
end

test "renders function components from dead layout", %{conn: conn} do
conn = get(conn, "/controller/render-layout-with-function-component")
assert html_response(conn, 200) =~ """
LAYOUT:COMPONENT:from layout
Hello
"""
end
end
235 changes: 203 additions & 32 deletions test/phoenix_live_view/diff_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,54 @@ defmodule Phoenix.LiveView.DiffTest do
end
end

defmodule BlockNoArgsComponent do
use Phoenix.LiveComponent

def mount(socket) do
{:ok, assign(socket, id: "DEFAULT")}
end

def render(%{do: _}), do: raise("unexpected :do assign")

def render(assigns) do
~L"""
HELLO <%= @id %> <%= render_block(@inner_block) %>
HELLO <%= @id %> <%= render_block(@inner_block) %>
"""
end
end

defmodule FunctionComponent do
def render_only(assigns) do
~L"""
RENDER ONLY <%= @from %>
"""
end

def render_with_block_no_args(assigns) do
~L"""
HELLO <%= @id %> <%= render_block(@inner_block) %>
HELLO <%= @id %> <%= render_block(@inner_block) %>
"""
end

def render_with_block(assigns) do
~L"""
HELLO <%= @id %> <%= render_block(@inner_block, 1) %>
HELLO <%= @id %> <%= render_block(@inner_block, 2) %>
"""
end

def render_with_live_component(assigns) do
~L"""
COMPONENT
<%= live_component :fake_socket, BlockComponent, id: "WORLD" do %>
WITH VALUE <%= @value %>
<% end %>
"""
end
end

defmodule TreeComponent do
use Phoenix.LiveComponent

Expand Down Expand Up @@ -520,8 +568,8 @@ defmodule Phoenix.LiveView.DiffTest do
assigns = %{socket: %Socket{}}

rendered = ~L"""
<%= live_component @socket, BlockComponent do %>
WITH VALUE <%= @value %>
<%= live_component @socket, BlockNoArgsComponent do %>
INSIDE BLOCK
<% end %>
"""

Expand All @@ -530,16 +578,165 @@ defmodule Phoenix.LiveView.DiffTest do
assert full_render == %{
0 => %{
0 => "",
1 => %{0 => "1", :s => ["\n WITH VALUE ", "\n"]},
1 => %{s: ["\n INSIDE BLOCK\n"]},
2 => "",
3 => %{0 => "2", :s => ["\n WITH VALUE ", "\n"]},
3 => %{s: ["\n INSIDE BLOCK\n"]},
:s => ["HELLO ", " ", "\nHELLO ", " ", "\n"]
},
:s => ["", "\n"]
}

{_socket, full_render, _components} = render(rendered, socket.fingerprints, components)
assert full_render == %{0 => %{0 => "", 1 => %{0 => "1"}, 2 => "", 3 => %{0 => "2"}}}
assert full_render == %{0 => %{0 => "", 2 => ""}}
end
end

describe "function components" do
test "render only" do
assigns = %{socket: %Socket{}}

rendered = ~L"""
<%= component &FunctionComponent.render_only/1, from: :component %>
"""

{socket, full_render, components} = render(rendered)

assert full_render == %{
0 => %{
0 => "component",
:s => ["RENDER ONLY ", "\n"]
},
:s => ["", "\n"]
}

assert socket.fingerprints != {rendered.fingerprint, %{}}
assert components == Diff.new_components()
end

test "block tracking without args" do
assigns = %{socket: %Socket{}}

rendered = ~L"""
<%= component &FunctionComponent.render_with_block_no_args/1, id: "DEFAULT" do %>
INSIDE BLOCK
<% end %>
"""

{socket, full_render, components} = render(rendered)

assert full_render == %{
0 => %{
0 => "DEFAULT",
1 => %{s: ["\n INSIDE BLOCK\n"]},
2 => "DEFAULT",
3 => %{s: ["\n INSIDE BLOCK\n"]},
:s => ["HELLO ", " ", "\nHELLO ", " ", "\n"]
},
:s => ["", "\n"]
}

{_socket, full_render, _components} = render(rendered, socket.fingerprints, components)
assert full_render == %{0 => %{0 => "DEFAULT", 2 => "DEFAULT"}}
end

defp function_tracking(assigns) do
~L"""
<%= component &FunctionComponent.render_with_block/1, id: @id do %>
<% value -> %>
WITH VALUE <%= value %> - <%= @value %>
<% end %>
"""
end

test "block tracking with args and parent assign" do
assigns = %{socket: %Socket{}, value: 123, id: "DEFAULT"}

{socket, full_render, components} = render(function_tracking(assigns))

assert full_render == %{
0 => %{
0 => "DEFAULT",
1 => %{0 => "1", :s => ["\n WITH VALUE ", " - ", "\n"], 1 => "123"},
2 => "DEFAULT",
3 => %{0 => "2", :s => ["\n WITH VALUE ", " - ", "\n"], 1 => "123"},
:s => ["HELLO ", " ", "\nHELLO ", " ", "\n"]
},
:s => ["", "\n"]
}

{_socket, full_render, _components} =
render(function_tracking(assigns), socket.fingerprints, components)

assert full_render == %{
0 => %{
0 => "DEFAULT",
1 => %{0 => "1", 1 => "123"},
2 => "DEFAULT",
3 => %{0 => "2", 1 => "123"}
}
}

assigns = Map.put(assigns, :__changed__, %{})

{_socket, full_render, _components} =
render(function_tracking(assigns), socket.fingerprints, components)

assert full_render == %{}

assigns = Map.put(assigns, :__changed__, %{id: true})

{_socket, full_render, _components} =
render(function_tracking(assigns), socket.fingerprints, components)

assert full_render == %{
0 => %{
0 => "DEFAULT",
1 => %{0 => "1"},
2 => "DEFAULT",
3 => %{0 => "2"}
}
}

assigns = Map.put(assigns, :__changed__, %{value: true})

{_socket, full_render, _components} =
render(function_tracking(assigns), socket.fingerprints, components)

assert full_render == %{
0 => %{
0 => "DEFAULT",
1 => %{0 => "1", 1 => "123"},
2 => "DEFAULT",
3 => %{0 => "2", 1 => "123"}
}
}
end

test "with live_component" do
assigns = %{socket: %Socket{}}

rendered = ~L"""
<%= component &FunctionComponent.render_with_live_component/1 %>
"""

{socket, full_render, components} = render(rendered)

assert full_render == %{
0 => %{0 => 1, :s => ["COMPONENT\n", "\n"]},
:c => %{
1 => %{
0 => "WORLD",
1 => %{0 => "1", :s => ["\n WITH VALUE ", "\n"]},
2 => "WORLD",
3 => %{0 => "2", :s => ["\n WITH VALUE ", "\n"]},
:s => ["HELLO ", " ", "\nHELLO ", " ", "\n"]
}
},
:s => ["", "\n"]
}

{_socket, full_render, _components} = render(rendered, socket.fingerprints, components)
assert full_render == %{0 => %{0 => 1}}
end
end

Expand Down Expand Up @@ -1172,33 +1369,6 @@ defmodule Phoenix.LiveView.DiffTest do
assert full_render == %{0 => 1}
end

test "explicit block tracking" do
assigns = %{socket: %Socket{}}

rendered = ~L"""
<%= live_component @socket, BlockComponent, id: "WORLD" do %>
<% extra -> %>
WITH EXTRA <%= inspect(extra) %>
<% end %>
"""

{_socket, full_render, _components} = render(rendered)

assert full_render == %{
0 => 1,
:c => %{
1 => %{
0 => "WORLD",
1 => %{0 => "[value: 1]", :s => ["\n WITH EXTRA ", "\n"]},
2 => "WORLD",
3 => %{0 => "[value: 2]", :s => ["\n WITH EXTRA ", "\n"]},
:s => ["HELLO ", " ", "\nHELLO ", " ", "\n"]
}
},
:s => ["", "\n"]
}
end

defp tracking(assigns) do
~L"""
<%= live_component @socket, BlockComponent, %{id: "TRACKING"} do %>
Expand All @@ -1208,6 +1378,7 @@ defmodule Phoenix.LiveView.DiffTest do
"""
end

# TODO: Change this to "with args and parent assign" once we deprecate implicit assigns
test "block tracking with child and parent assigns" do
assigns = %{socket: %Socket{}, parent_value: 123}
{socket, full_render, components} = render(tracking(assigns))
Expand Down
Loading

0 comments on commit 3356970

Please sign in to comment.