diff --git a/lib/phoenix_live_view.ex b/lib/phoenix_live_view.ex
index c9d8e950a7..8ac045bfeb 100644
--- a/lib/phoenix_live_view.ex
+++ b/lib/phoenix_live_view.ex
@@ -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.
diff --git a/lib/phoenix_live_view/async.ex b/lib/phoenix_live_view/async.ex
index e67e47a6ee..2b32b8a068 100644
--- a/lib/phoenix_live_view/async.ex
+++ b/lib/phoenix_live_view/async.ex
@@ -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
diff --git a/lib/phoenix_live_view/lifecycle.ex b/lib/phoenix_live_view/lifecycle.ex
index a1d855d4d2..c6e3862d64 100644
--- a/lib/phoenix_live_view/lifecycle.ex
+++ b/lib/phoenix_live_view/lifecycle.ex
@@ -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`.
@@ -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
@@ -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))
@@ -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)
@@ -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)}
"""
@@ -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} =
@@ -224,7 +237,7 @@ defmodule Phoenix.LiveView.Lifecycle do
Expected one of:
- #{expected_return(hook)}
+ #{expected_return(hook)}
Got: #{inspect(result)}
"""
diff --git a/test/phoenix_live_view/hooks_test.exs b/test/phoenix_live_view/hooks_test.exs
index 5af51c0f94..00c715ef12 100644
--- a/test/phoenix_live_view/hooks_test.exs
+++ b/test/phoenix_live_view/hooks_test.exs
@@ -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()
@@ -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}]
diff --git a/test/phoenix_live_view/integrations/hooks_test.exs b/test/phoenix_live_view/integrations/hooks_test.exs
index 55859ab5a4..2a22f801cb 100644
--- a/test/phoenix_live_view/integrations/hooks_test.exs
+++ b/test/phoenix_live_view/integrations/hooks_test.exs
@@ -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
@@ -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")
@@ -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,
diff --git a/test/support/live_views/lifecycle.ex b/test/support/live_views/lifecycle.ex
index 17f466cc16..8bd53e9e23 100644
--- a/test/support/live_views/lifecycle.ex
+++ b/test/support/live_views/lifecycle.ex
@@ -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 %>
+
"""
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))}
@@ -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
@@ -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