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

Handle async hooks #2859

Merged
merged 4 commits into from
Dec 18, 2023
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: 1 addition & 1 deletion lib/phoenix_live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,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 @@ -129,16 +129,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
Loading