Skip to content

Commit

Permalink
Support handle_event hooks on LiveComponents (#3158)
Browse files Browse the repository at this point in the history
LiveComponents can now attach `:handle_event` hooks. The previous tests
have been updated to use `handle_info` which will likely not be
supported by LiveComponents as they don't have a `:handle_info`.
  • Loading branch information
Gazler authored Mar 11, 2024
1 parent 7554302 commit c667462
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 28 deletions.
3 changes: 2 additions & 1 deletion lib/phoenix_live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,8 @@ defmodule Phoenix.LiveView do
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.
> Note: only `:after_render` and `:handle_event` hooks are currently supported in
> LiveComponents.
## Return Values
Expand Down
34 changes: 21 additions & 13 deletions lib/phoenix_live_view/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ defmodule Phoenix.LiveView.Channel do
new_socket
end


{new_socket, {:ok, {msg.ref, %{}}, state}}

other ->
Expand Down Expand Up @@ -727,24 +726,33 @@ defmodule Phoenix.LiveView.Channel do
fn ->
component_socket =
%Socket{redirected: redirected, assigns: assigns} =
case component.handle_event(event, val, component_socket) do
{:noreply, component_socket} ->
case Lifecycle.handle_event(event, val, component_socket) do
{:halt, %Socket{} = component_socket} ->
component_socket

{:reply, %{} = reply, component_socket} ->
Utils.put_reply(component_socket, reply)
{:cont, %Socket{} = component_socket} ->
case component.handle_event(event, val, component_socket) do
{:noreply, component_socket} ->
component_socket

other ->
raise ArgumentError, """
invalid return from #{inspect(component)}.handle_event/3 callback.
{:reply, %{} = reply, component_socket} ->
Utils.put_reply(component_socket, reply)

Expected one of:
other ->
raise ArgumentError, """
invalid return from #{inspect(component)}.handle_event/3 callback.
{:noreply, %Socket{}}
{:reply, map, %Socket}
Expected one of:
Got: #{inspect(other)}
"""
{:noreply, %Socket{}}
{:reply, map, %Socket}
Got: #{inspect(other)}
"""
end

other ->
raise_bad_callback_response!(other, component_socket.view, :handle_event, 3)
end

new_component_socket =
Expand Down
2 changes: 1 addition & 1 deletion lib/phoenix_live_view/lifecycle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ defmodule Phoenix.LiveView.Lifecycle do
end

defp lifecycle(socket, stage) do
if Utils.cid(socket) && stage not in [:after_render] do
if Utils.cid(socket) && stage not in [:after_render, :handle_event] do
raise ArgumentError, "lifecycle hooks are not supported on stateful components."
end

Expand Down
26 changes: 24 additions & 2 deletions test/phoenix_live_view/integrations/hooks_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -295,16 +295,38 @@ defmodule Phoenix.LiveView.HooksTest do
assert render_async(lv) =~ "task:o.\n"
end

test "attach/detach_hook with a handle_event live component socket", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/lifecycle/components/handle_event")
lv |> element("#attach") |> render_click()
lv |> element("#hook") |> render_click()
assert render_async(lv) =~ "counter: 1"

lv |> element("#hook") |> render_click()
assert render_async(lv) =~ "counter: 2"

lv |> element("#detach-component-hook") |> render_click()
Process.flag(:trap_exit, true)

assert ExUnit.CaptureLog.capture_log(fn ->
try do
lv |> element("#hook") |> render_click()
catch
:exit, _ -> :ok
end
end) =~
"** (UndefinedFunctionError) function Phoenix.LiveViewTest.HooksEventComponent.handle_event/3 is undefined"
end

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

assert HooksLive.exits_with(lv, ArgumentError, fn ->
lv |> element("#attach") |> render_click()
end) =~ "lifecycle hooks are not supported on stateful components."
end

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

assert HooksLive.exits_with(lv, ArgumentError, fn ->
lv |> element("#detach") |> render_click()
Expand Down
49 changes: 39 additions & 10 deletions test/support/live_views/lifecycle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,12 @@ defmodule Phoenix.LiveViewTest.HaltConnectedMount do
end
end

defmodule Phoenix.LiveViewTest.HooksAttachComponent do
defmodule Phoenix.LiveViewTest.HooksAttachInfoComponent do
use Phoenix.LiveComponent
alias Phoenix.LiveView

def mount(socket) do
{:ok, LiveView.attach_hook(socket, :live_component_hook, :handle_event, &__MODULE__.hook/3)}
{:ok, LiveView.attach_hook(socket, :live_component_hook, :handle_info, &__MODULE__.hook/3)}
end

def hook(_, _, _socket) do
Expand All @@ -223,35 +223,64 @@ defmodule Phoenix.LiveViewTest.HooksAttachComponent do
def render(assigns), do: ~H"<div></div>"
end

defmodule Phoenix.LiveViewTest.HooksDetachComponent do
defmodule Phoenix.LiveViewTest.HooksDetachInfoComponent do
use Phoenix.LiveComponent
alias Phoenix.LiveView

def mount(socket) do
{:ok, LiveView.detach_hook(socket, :live_view_hook, :handle_event)}
{:ok, LiveView.detach_hook(socket, :live_view_hook, :handle_info)}
end

def render(assigns), do: ~H"<div></div>"
end

defmodule Phoenix.LiveViewTest.HooksEventComponent do
use Phoenix.LiveComponent
alias Phoenix.LiveView

def mount(socket) do
socket = assign(socket, :counter, 0)
{:ok, LiveView.attach_hook(socket, :live_component_hook, :handle_event, &__MODULE__.hook/3)}
end

def hook("detach", _, socket),
do: {:halt, LiveView.detach_hook(socket, :live_component_hook, :handle_event)}

def hook(_, _, socket), do: {:halt, assign(socket, :counter, socket.assigns.counter + 1)}

def render(assigns) do
~H"""
<div>
<div id="detach-component-hook" phx-click="detach" phx-target={@myself}>Detach</div>
<div id="hook" phx-click="event" phx-target={@myself}>counter: <%= @counter %></div>
</div>
"""
end
end

defmodule Phoenix.LiveViewTest.HooksLive.WithComponent do
use Phoenix.LiveView, namespace: Phoenix.LiveViewTest
alias Phoenix.LiveViewTest.{HooksAttachComponent, HooksDetachComponent}
alias Phoenix.LiveViewTest.{HooksAttachInfoComponent, HooksDetachInfoComponent}
alias Phoenix.LiveViewTest.HooksEventComponent

def mount(params, _session, socket) do
type = String.to_existing_atom(params["type"])

def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:component, nil)
|> assign(:type, type)
|> attach_hook(:live_view_hook, :handle_event, fn _, _, socket ->
{:cont, socket}
end)}
end

def handle_event("load", %{"val" => val}, socket) do
component =
case val do
"attach" -> HooksAttachComponent
"detach" -> HooksDetachComponent
case {val, socket.assigns.type} do
{"attach", :handle_info} -> HooksAttachInfoComponent
{"detach", :handle_info} -> HooksDetachInfoComponent
{"attach", :handle_event} -> HooksEventComponent
end

{:noreply, assign(socket, :component, component)}
Expand All @@ -262,7 +291,7 @@ defmodule Phoenix.LiveViewTest.HooksLive.WithComponent do
<button id="attach" phx-click="load" phx-value-val="attach">Load/Attach</button>
<button id="detach" phx-click="load" phx-value-val="detach">Load/Detach</button>
<%= if @component do %>
<.live_component module={@component} id={:hook} />
<.live_component module={@component} id={:hook} type={@type} />
<% end %>
"""
end
Expand Down
2 changes: 1 addition & 1 deletion test/support/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ defmodule Phoenix.LiveViewTest.Router do
live "/lifecycle/halt-mount", HooksLive.HaltMount
live "/lifecycle/redirect-cont-mount", HooksLive.RedirectMount, :cont
live "/lifecycle/redirect-halt-mount", HooksLive.RedirectMount, :halt
live "/lifecycle/components", HooksLive.WithComponent
live "/lifecycle/components/:type", HooksLive.WithComponent
live "/lifecycle/handle-params-not-defined", HooksLive.HandleParamsNotDefined
live "/lifecycle/handle-info-not-defined", HooksLive.HandleInfoNotDefined
live "/lifecycle/on-mount-options", HooksLive.OnMountOptions
Expand Down

0 comments on commit c667462

Please sign in to comment.