Skip to content

Commit

Permalink
Support :myself assign in send_update/3
Browse files Browse the repository at this point in the history
  • Loading branch information
dvic committed Jul 20, 2023
1 parent 5e63a13 commit 5afb2d9
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 29 deletions.
42 changes: 33 additions & 9 deletions lib/phoenix_live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1286,20 +1286,24 @@ defmodule Phoenix.LiveView do
@doc """
Asynchronously updates a `Phoenix.LiveComponent` with new assigns.
The `:id` that identifies the component must be passed as part of the
assigns and it will be used to identify the live components to be updated.
The `pid` argument is optional and it defaults to the current process,
which means the update instruction will be sent to a component running
on the same LiveView. If the current process is not a LiveView or you
want to send updates to a live component running on another LiveView,
you should explicitly pass the LiveView's pid instead.
The second argument can be either the value of the `@myself` or the module of
the live component. If you pass the module, then the `:id` that identifies
the component must be passed as part of the assigns.
When the component receives the update, first the optional
[`preload/1`](`c:Phoenix.LiveComponent.preload/1`) then
[`update/2`](`c:Phoenix.LiveComponent.update/2`) is invoked with the new assigns.
If [`update/2`](`c:Phoenix.LiveComponent.update/2`) is not defined
all assigns are simply merged into the socket. The assigns received as the first argument of the [`update/2`](`c:Phoenix.LiveComponent.update/2`) callback will only include the _new_ assigns passed from this function. Pre-existing assigns may be found in `socket.assigns`.
all assigns are simply merged into the socket. The assigns received as the
first argument of the [`update/2`](`c:Phoenix.LiveComponent.update/2`)
callback will only include the _new_ assigns passed from this function.
Pre-existing assigns may be found in `socket.assigns`.
While a component may always be updated from the parent by updating some
parent assigns which will re-render the child, thus invoking
Expand All @@ -1309,7 +1313,6 @@ defmodule Phoenix.LiveView do
LiveView.
## Examples
def handle_event("cancel-order", _, socket) do
...
send_update(Cart, id: "cart", status: "cancelled")
Expand All @@ -1327,15 +1330,27 @@ defmodule Phoenix.LiveView do
{:noreply, socket}
end
def render(assigns) do
<.some_component on_complete={&send_update(@myself, completed: &1)} />
end
"""
def send_update(pid \\ self(), module, assigns) when is_atom(module) and is_pid(pid) do
def send_update(pid \\ self(), module_or_cid, assigns)

def send_update(pid, module, assigns) when is_atom(module) and is_pid(pid) do
assigns = Enum.into(assigns, %{})

id =
assigns[:id] ||
raise ArgumentError, "missing required :id in send_update. Got: #{inspect(assigns)}"

Phoenix.LiveView.Channel.send_update(pid, module, id, assigns)
Phoenix.LiveView.Channel.send_update(pid, {module, id}, assigns)
end

def send_update(pid, %Phoenix.LiveComponent.CID{} = cid, assigns) when is_pid(pid) do
assigns = Enum.into(assigns, %{})

Phoenix.LiveView.Channel.send_update(pid, cid, assigns)
end

@doc """
Expand All @@ -1361,15 +1376,24 @@ defmodule Phoenix.LiveView do
{:noreply, socket}
end
"""
def send_update_after(pid \\ self(), module, assigns, time_in_milliseconds)
def send_update_after(pid \\ self(), module_or_cid, assigns, time_in_milliseconds)

def send_update_after(pid, %Phoenix.LiveComponent.CID{} = cid, assigns, time_in_milliseconds)
when is_integer(time_in_milliseconds) and is_pid(pid) do
assigns = Enum.into(assigns, %{})

Phoenix.LiveView.Channel.send_update_after(pid, cid, assigns, time_in_milliseconds)
end

def send_update_after(pid, module, assigns, time_in_milliseconds)
when is_atom(module) and is_integer(time_in_milliseconds) and is_pid(pid) do
assigns = Enum.into(assigns, %{})

id =
assigns[:id] ||
raise ArgumentError, "missing required :id in send_update_after. Got: #{inspect(assigns)}"

Phoenix.LiveView.Channel.send_update_after(pid, module, id, assigns, time_in_milliseconds)
Phoenix.LiveView.Channel.send_update_after(pid, {module, id}, assigns, time_in_milliseconds)
end

@doc """
Expand Down
37 changes: 23 additions & 14 deletions lib/phoenix_live_view/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ defmodule Phoenix.LiveView.Channel do
GenServer.start_link(__MODULE__, from, opts)
end

def send_update(pid \\ self(), module, id, assigns) do
send(pid, {@prefix, :send_update, {module, id, assigns}})
def send_update(pid, ref, assigns) do
send(pid, {@prefix, :send_update, {ref, assigns}})
end

def send_update_after(pid \\ self(), module, id, assigns, time_in_milliseconds)
def send_update_after(pid, ref, assigns, time_in_milliseconds)
when is_integer(time_in_milliseconds) do
Process.send_after(
pid,
{@prefix, :send_update, {module, id, assigns}},
{@prefix, :send_update, {ref, assigns}},
time_in_milliseconds
)
end
Expand Down Expand Up @@ -241,16 +241,7 @@ defmodule Phoenix.LiveView.Channel do
{:noreply, push_diff(%{state | components: new_components}, diff, nil)}

:noop ->
{module, id, _} = update

if exported?(module, :__info__, 1) do
# Only a warning, because there can be race conditions where a component is removed before a `send_update` happens.
Logger.debug(
"send_update failed because component #{inspect(module)} with ID #{inspect(id)} does not exist or it has been removed"
)
else
raise ArgumentError, "send_update failed (module #{inspect(module)} is not available)"
end
handle_noop(update)

{:noreply, state}
end
Expand All @@ -277,6 +268,24 @@ defmodule Phoenix.LiveView.Channel do
|> handle_result({:handle_info, 2, nil}, state)
end

defp handle_noop({%Phoenix.LiveComponent.CID{cid: cid}, _}) do
# Only a warning, because there can be race conditions where a component is removed before a `send_update` happens.
Logger.debug(
"send_update failed because component with CID #{inspect(cid)} does not exist or it has been removed"
)
end

defp handle_noop({{module, id}, _}) do
if exported?(module, :__info__, 1) do
# Only a warning, because there can be race conditions where a component is removed before a `send_update` happens.
Logger.debug(
"send_update failed because component #{inspect(module)} with ID #{inspect(id)} does not exist or it has been removed"
)
else
raise ArgumentError, "send_update failed (module #{inspect(module)} is not available)"
end
end

@impl true
def handle_call({@prefix, :ping}, _from, state) do
{:reply, :ok, state}
Expand Down
20 changes: 15 additions & 5 deletions lib/phoenix_live_view/diff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ defmodule Phoenix.LiveView.Diff do
{diff, new_components} = Diff.update_component(socket, state.components, update)
"""
def update_component(socket, components, {module, id, updated_assigns}) do
case fetch_cid(module, id, components) do
{:ok, cid} ->
def update_component(socket, components, {ref, updated_assigns}) do
case fetch_cid(ref, components) do
{:ok, {cid, module}} ->
updated_assigns = maybe_call_preload!(module, updated_assigns)

{diff, new_components, :noop} =
Expand Down Expand Up @@ -787,9 +787,19 @@ defmodule Phoenix.LiveView.Diff do
{id_to_components, Map.put(id_to_cid, component, Map.put(inner, id, cid)), uuids}
end

defp fetch_cid(component, id, {_cid_to_components, id_to_cid, _} = _components) do
defp fetch_cid(
%Phoenix.LiveComponent.CID{cid: cid},
{cid_to_components, _id_to_cid, _} = _components
) do
case cid_to_components do
%{^cid => {component, _id, _assigns, _private, _fingerprints}} -> {:ok, {cid, component}}
%{} -> :error
end
end

defp fetch_cid({component, id}, {_cid_to_components, id_to_cid, _} = _components) do
case id_to_cid do
%{^component => %{^id => cid}} -> {:ok, cid}
%{^component => %{^id => cid}} -> {:ok, {cid, component}}
%{} -> :error
end
end
Expand Down
21 changes: 21 additions & 0 deletions test/phoenix_live_view/integrations/live_components_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,19 @@ defmodule Phoenix.LiveView.LiveComponentsTest do
refute_receive {:updated, _}
end

test "updates with cid", %{conn: conn} do
{:ok, view, _html} = live(conn, "/components")

Phoenix.LiveView.send_update_after(view.pid, StatefulComponent, [id: "jose", name: "NEW-jose", from: self(), all_assigns: true], 10)
assert_receive {:updated, %{id: "jose", name: "NEW-jose", myself: myself}}

Phoenix.LiveView.send_update(view.pid, myself, [name: "NEXTGEN-jose", from: self()])
assert_receive {:updated, %{id: "jose", name: "NEXTGEN-jose"}}

Phoenix.LiveView.send_update_after(view.pid, myself, [name: "after-NEXTGEN-jose", from: self()], 10)
assert_receive {:updated, %{id: "jose", name: "after-NEXTGEN-jose"}}, 500
end

test "updates without :id raise", %{conn: conn} do
Process.flag(:trap_exit, true)
{:ok, view, _html} = live(conn, "/components")
Expand All @@ -315,12 +328,20 @@ defmodule Phoenix.LiveView.LiveComponentsTest do
test "warns if component doesn't exist", %{conn: conn} do
{:ok, view, _html} = live(conn, "/components")

# with module and id
assert ExUnit.CaptureLog.capture_log(fn ->
send(view.pid, {:send_update, [{StatefulComponent, id: "nemo", name: "NEW-nemo"}]})
render(view)
refute_receive {:updated, _}
end) =~
"send_update failed because component Phoenix.LiveViewTest.StatefulComponent with ID \"nemo\" does not exist or it has been removed"

# with @myself
assert ExUnit.CaptureLog.capture_log(fn ->
send(view.pid, {:send_update, [{%Phoenix.LiveComponent.CID{cid: 999}, name: "NEW-nemo"}]})
render(view)
refute_receive {:updated, _}
end) =~ "send_update failed because component with CID 999 does not exist or it has been removed"
end

test "raises if component module is not available", %{conn: conn} do
Expand Down
3 changes: 2 additions & 1 deletion test/support/live_views/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ defmodule Phoenix.LiveViewTest.StatefulComponent do

def update(assigns, socket) do
if from = assigns[:from] do
send(from, {:updated, assigns})
sent_assigns = Map.merge(assigns, %{id: socket.assigns[:id], myself: socket.assigns.myself})
send(from, {:updated, sent_assigns})
end

{:ok, assign(socket, assigns)}
Expand Down

0 comments on commit 5afb2d9

Please sign in to comment.