Skip to content

Commit

Permalink
Allow handle async hooks (#2859)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcastano authored Dec 18, 2023
1 parent 8a5315d commit 8a20fc6
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 22 deletions.
2 changes: 1 addition & 1 deletion lib/phoenix_live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1375,7 +1375,7 @@ defmodule Phoenix.LiveView do
lifecycle in order to bind/update assigns, intercept events,
patches, and regular messages when necessary, and to inject
common functionality. Use `attach_hook/1` on any of the following
lifecycle stages: `:handle_params`, `:handle_event`, `:handle_info`, and
lifecycle stages: `:handle_params`, `:handle_event`, `:handle_info`, `:handle_async`, and
`:after_render`. To attach a hook to the `:mount` stage, use `on_mount/1`.
> Note: only `:after_render` hooks are currently supported in LiveComponents.
Expand Down
22 changes: 14 additions & 8 deletions lib/phoenix_live_view/async.ex
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,22 @@ defmodule Phoenix.LiveView.Async do
defp handle_kind(socket, maybe_component, :start, key, result) do
callback_mod = maybe_component || socket.view

case callback_mod.handle_async(key, result, socket) do
{:noreply, %Socket{} = new_socket} ->
new_socket
case Phoenix.LiveView.Lifecycle.handle_async(key, result, socket) do
{:cont, %Socket{} = socket} ->
case callback_mod.handle_async(key, result, socket) do
{:noreply, %Socket{} = new_socket} ->
new_socket

other ->
raise ArgumentError, """
expected #{inspect(callback_mod)}.handle_async/3 to return {:noreply, socket}, got:
other ->
raise ArgumentError, """
expected #{inspect(callback_mod)}.handle_async/3 to return {:noreply, socket}, got:
#{inspect(other)}
"""
#{inspect(other)}
"""
end

{_, %Socket{} = socket} ->
socket
end
end

Expand Down
29 changes: 21 additions & 8 deletions lib/phoenix_live_view/lifecycle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ defmodule Phoenix.LiveView.Lifecycle do
@type hook :: map()

@type t :: %__MODULE__{
after_render: [hook],
handle_async: [hook],
handle_event: [hook],
handle_info: [hook],
handle_params: [hook],
after_render: [hook],
mount: [hook]
}

defstruct handle_event: [], handle_info: [], handle_params: [], mount: [], after_render: []
defstruct after_render: [],
handle_async: [],
handle_event: [],
handle_info: [],
handle_params: [],
mount: []

@doc """
Returns a map of infos about the lifecycle stage for the given `view`.
Expand All @@ -31,7 +37,7 @@ defmodule Phoenix.LiveView.Lifecycle do
end

defp callbacks?(%Socket{private: %{@lifecycle => lifecycle}}, stage)
when stage in [:handle_event, :handle_info, :handle_params, :mount] do
when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :mount] do
lifecycle |> Map.fetch!(stage) |> Kernel.!=([])
end

Expand All @@ -41,7 +47,7 @@ defmodule Phoenix.LiveView.Lifecycle do
end

def attach_hook(%Socket{} = socket, id, stage, fun)
when stage in [:handle_event, :handle_info, :handle_params, :after_render] do
when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :after_render] do
lifecycle = lifecycle(socket, stage)
hook = hook!(id, stage, fun)
existing = Enum.find(Map.fetch!(lifecycle, stage), &(&1.id == id))
Expand All @@ -61,14 +67,14 @@ defmodule Phoenix.LiveView.Lifecycle do
raise ArgumentError, """
invalid lifecycle event provided to attach_hook.
Expected one of: :handle_event | :handle_info | :handle_params
Expected one of: :handle_async | :handle_event | :handle_info | :handle_params | :after_render
Got: #{inspect(stage)}
"""
end

def detach_hook(%Socket{} = socket, id, stage)
when stage in [:handle_event, :handle_info, :handle_params, :after_render] do
when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :after_render] do
update_lifecycle(socket, stage, fn hooks ->
for hook <- hooks, hook.id != id, do: hook
end)
Expand All @@ -78,7 +84,7 @@ defmodule Phoenix.LiveView.Lifecycle do
raise ArgumentError, """
invalid lifecycle event provided to detach_hook.
Expected one of: :handle_event | :handle_info | :handle_params
Expected one of: :handle_async | :handle_event | :handle_info | :handle_params | :after_render
Got: #{inspect(stage)}
"""
Expand Down Expand Up @@ -191,6 +197,13 @@ defmodule Phoenix.LiveView.Lifecycle do
end)
end

@doc false
def handle_async(key, result, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
reduce_socket(lifecycle.handle_async, socket, fn hook, acc ->
hook.function.(key, result, acc)
end)
end

@doc false
def after_render(%Socket{private: %{@lifecycle => lifecycle}} = socket) do
{:cont, new_socket} =
Expand Down Expand Up @@ -224,7 +237,7 @@ defmodule Phoenix.LiveView.Lifecycle do
Expected one of:
#{expected_return(hook)}
#{expected_return(hook)}
Got: #{inspect(result)}
"""
Expand Down
9 changes: 9 additions & 0 deletions test/phoenix_live_view/hooks_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ defmodule Phoenix.LiveView.IntegrationHooksTest do
end
end

test "supports handle_async/3" do
assert %Lifecycle{handle_async: [%{id: :noop}]} =
build_socket()
|> LiveView.attach_hook(:noop, :handle_async, &noop/3)
|> lifecycle()
end

test "supports handle_event/3" do
assert %Lifecycle{handle_event: [%{id: :noop}]} =
build_socket()
Expand Down Expand Up @@ -62,11 +69,13 @@ defmodule Phoenix.LiveView.IntegrationHooksTest do
test "supports named hooks for multiple lifecycle events" do
socket =
build_socket()
|> LiveView.attach_hook(:noop, :handle_async, &noop/3)
|> LiveView.attach_hook(:noop, :handle_params, &noop/3)
|> LiveView.attach_hook(:noop, :handle_event, &noop/3)
|> LiveView.attach_hook(:noop, :handle_info, &noop/2)

assert %Lifecycle{
handle_async: [%{id: :noop, stage: :handle_async}],
handle_info: [%{id: :noop, stage: :handle_info}],
handle_event: [%{id: :noop, stage: :handle_event}],
handle_params: [%{id: :noop, stage: :handle_params}]
Expand Down
56 changes: 52 additions & 4 deletions test/phoenix_live_view/integrations/hooks_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,10 @@ defmodule Phoenix.LiveView.HooksTest do
{:halt, %{}, socket}
end)

%{proxy: {_ref, _topic, proxy_pid}} = lv
Process.unlink(proxy_pid)
ref = HooksLive.unlink_and_monitor(lv)

assert ExUnit.CaptureLog.capture_log(fn ->
send(lv.pid, :boom)
ref = Process.monitor(lv.pid)
assert_receive {:DOWN, ^ref, _, _, _}
end) =~ "Got: {:halt, %{}, #Phoenix.LiveView.Socket<"
end
Expand Down Expand Up @@ -242,11 +240,55 @@ defmodule Phoenix.LiveView.HooksTest do
refute_received {:intercepted, ^ref}
end

test "handle_info/3 without module callback", %{conn: conn} do
test "handle_info/2 without module callback", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/lifecycle/handle-info-not-defined")
assert render(lv) =~ "data=somedata"
end

test "handle_async/3 raises when hook result is invalid", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/lifecycle")

HooksLive.attach_hook(lv, :boom, :handle_async, fn _, _, _ -> :boom end)

monitor = HooksLive.unlink_and_monitor(lv)
lv |> element("#async") |> render_click()
assert_receive {:DOWN, ^monitor, :process, _pid, {%error{message: msg}, _}}
assert error == ArgumentError
assert msg =~ "Got: :boom"
end

test "handle_async/3 attached after connected", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/lifecycle")

HooksLive.attach_hook(lv, :hook, :handle_async, fn _, _, socket ->
{:cont, Component.update(socket, :task, &(&1 <> "o"))}
end)

lv |> element("#async") |> render_click()
assert render_async(lv) =~ "task:o.\n"

HooksLive.detach_hook(lv, :hook, :handle_async)

lv |> element("#async") |> render_click()
assert render_async(lv) =~ "task:o..\n"
end

test "handle_async/3 halts", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/lifecycle")

HooksLive.attach_hook(lv, :hook, :handle_async, fn _, _, socket ->
{:halt, Component.update(socket, :task, &(&1 <> "o"))}
end)

lv |> element("#async") |> render_click()
assert render_async(lv) =~ "task:o\n"

HooksLive.detach_hook(lv, :hook, :handle_async)

lv |> element("#async") |> render_click()
assert render_async(lv) =~ "task:o.\n"
end

test "attach_hook raises when given a live component socket", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/lifecycle/components")

Expand Down Expand Up @@ -275,6 +317,12 @@ defmodule Phoenix.LiveView.HooksTest do
exported?: true
}

assert Lifecycle.stage_info(socket, HooksLive, :handle_async, 3) == %{
any?: true,
callbacks?: false,
exported?: true
}

assert Lifecycle.stage_info(socket, HooksLive, :handle_params, 3) == %{
any?: false,
callbacks?: false,
Expand Down
17 changes: 16 additions & 1 deletion test/support/live_views/lifecycle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,16 @@ defmodule Phoenix.LiveViewTest.HooksLive do
last_on_mount:<%= inspect(assigns[:last_on_mount]) %>
params_hook:<%= assigns[:params_hook_ref] %>
count:<%= @count %>
task:<%= @task %>
<button id="dec" phx-click="dec">-</button>
<button id="inc" phx-click="inc">+</button>
<button id="patch" phx-click="patch">?</button>
<button id="async" phx-click="async">=</button>
"""
end

def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
{:ok, assign(socket, count: 0, task: "")}
end

def handle_event("inc", _, socket), do: {:noreply, update(socket, :count, &(&1 + 1))}
Expand All @@ -75,6 +77,14 @@ defmodule Phoenix.LiveViewTest.HooksLive do
{:noreply, push_patch(socket, to: "/lifecycle?ref=#{ref}")}
end

def handle_event("async", _, socket) do
{:noreply, start_async(socket, :task, fn -> true end)}
end

def handle_async(:task, {:ok, true}, socket) do
{:noreply, update(socket, :task, &(&1 <> "."))}
end

def handle_call({:run, func}, _, socket), do: func.(socket)

def handle_call({:push_patch, to}, _, socket) do
Expand Down Expand Up @@ -121,6 +131,11 @@ defmodule Phoenix.LiveViewTest.HooksLive do
end
end

def unlink_and_monitor(lv) do
Process.unlink(proxy_pid(lv))
Process.monitor(proxy_pid(lv))
end

def run(lv, func) do
GenServer.call(lv.pid, {:run, func})
end
Expand Down

0 comments on commit 8a20fc6

Please sign in to comment.