Skip to content
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

Support function components via component/3 #1428

Merged
merged 2 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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