Skip to content

Commit

Permalink
Add support for update_many/2
Browse files Browse the repository at this point in the history
  • Loading branch information
dvic committed Aug 15, 2023
1 parent 2d08b92 commit 358068c
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 46 deletions.
5 changes: 4 additions & 1 deletion lib/phoenix_live_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ defmodule Phoenix.LiveComponent do
@callback mount(socket :: Socket.t()) ::
{:ok, Socket.t()} | {:ok, Socket.t(), keyword()}

@callback update_many(list_of_assigns :: [Socket.assigns()], sockets :: [Socket.t()]) ::
[Socket.t()]

@callback preload(list_of_assigns :: [Socket.assigns()]) ::
list_of_assigns :: [Socket.assigns()]

Expand All @@ -530,5 +533,5 @@ defmodule Phoenix.LiveComponent do
) ::
{:noreply, Socket.t()} | {:reply, map, Socket.t()}

@optional_callbacks mount: 1, preload: 1, update: 2, handle_event: 3
@optional_callbacks mount: 1, update_many: 2, preload: 1, update: 2, handle_event: 3
end
103 changes: 78 additions & 25 deletions lib/phoenix_live_view/diff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -627,44 +627,97 @@ defmodule Phoenix.LiveView.Diff do

{{pending, diffs, components}, seen_ids} =
Enum.reduce(pending, acc, fn {component, entries}, acc ->
entries = maybe_preload_components(component, Enum.reverse(entries))
# reverse order so next reduce returns in original order
# maybe_preload_components might not touch entries if update_many is
# defined, so reverse them before
entries = Enum.reverse(entries)
entries = maybe_preload_components(component, entries)

{triplet, seen_ids} = acc
# {triplet, seen_ids, entries, sockets, list_of_assigns}
acc = {triplet, seen_ids, [], [], []}

# first collect sockets and list of assigns, because of update_many/2
{triplet, seen_ids, entries, sockets, list_of_assigns} =
Enum.reduce(entries, acc, fn {cid, id, _new?, new_assigns} = entry,
{triplet, seen_ids, entries, sockets, list_of_assigns} ->
{pending, diffs, components} = triplet

seen_ids = ensure_not_seen!(seen_ids, component, id)

{socket, components} = ensure_component(socket, component, cids, cid, id, components)

{
{pending, diffs, components},
seen_ids,
[entry | entries],
[socket | sockets],
[new_assigns | list_of_assigns]
}
end)

Enum.reduce(entries, acc, fn {cid, id, new?, new_assigns}, {triplet, seen_ids} ->
{pending, diffs, components} = triplet
# reverse order so next reduce returns in original order
entries = Enum.reverse(entries)
sockets = Enum.reverse(sockets)
list_of_assigns = Enum.reverse(list_of_assigns)

if Map.has_key?(seen_ids, [component | id]) do
raise "found duplicate ID #{inspect(id)} " <>
"for component #{inspect(component)} when rendering template"
# execute update/2 or update_many/2
sockets =
if function_exported?(component, :update_many, 2) do
Utils.update_many!(sockets, component, list_of_assigns)
else
sockets
|> Enum.zip(list_of_assigns)
|> Enum.map(fn {socket, assigns} ->
Utils.maybe_call_update!(socket, component, assigns)
end)
end

{socket, components} =
case cids do
%{^cid => {_component, _id, assigns, private, prints}} ->
private = Map.delete(private, @marked_for_deletion)
{configure_socket_for_component(socket, assigns, private, prints), components}

%{} ->
myself_assigns = %{myself: %Phoenix.LiveComponent.CID{cid: cid}}

{mount_component(socket, component, myself_assigns),
put_cid(components, component, id, cid)}
end
# render components
triplet =
entries
|> Enum.zip(sockets)
|> Enum.reduce(triplet, fn {entry, socket}, triplet ->
{cid, id, new?, _new_assigns} = entry
{pending, diffs, components} = triplet

socket = Utils.maybe_call_update!(socket, component, new_assigns)
diffs = maybe_put_events(diffs, socket)
diffs = maybe_put_events(diffs, socket)

triplet =
render_component(socket, component, id, cid, new?, pending, cids, diffs, components)
end)

{triplet, Map.put(seen_ids, [component | id], true)}
end)
{triplet, seen_ids}
end)

render_pending_components(socket, pending, seen_ids, cids, diffs, components)
end

defp ensure_component(socket, component, cids, cid, id, components) do
case cids do
%{^cid => {_component, _id, assigns, private, prints}} ->
private = Map.delete(private, @marked_for_deletion)
{configure_socket_for_component(socket, assigns, private, prints), components}

%{} ->
myself_assigns = %{myself: %Phoenix.LiveComponent.CID{cid: cid}}

{mount_component(socket, component, myself_assigns),
put_cid(components, component, id, cid)}
end
end

defp ensure_not_seen!(seen_ids, component, id) do
if Map.has_key?(seen_ids, [component | id]) do
raise "found duplicate ID #{inspect(id)} " <>
"for component #{inspect(component)} when rendering template"
end

Map.put(seen_ids, [component | id], true)
end

defp maybe_preload_components(component, entries) do
if function_exported?(component, :preload, 1) do
if function_exported?(component, :preload, 1) and
not function_exported?(component, :update_many, 2) do
list_of_assigns = Enum.map(entries, fn {_cid, _id, _new?, new_assigns} -> new_assigns end)
result = component.preload(list_of_assigns)
zip_preloads(result, entries, component, result)
Expand All @@ -674,7 +727,7 @@ defmodule Phoenix.LiveView.Diff do
end

defp maybe_call_preload!(module, assigns) do
if function_exported?(module, :preload, 1) do
if function_exported?(module, :preload, 1) and not function_exported?(module, :update_many, 2) do
[new_assigns] = module.preload([assigns])
new_assigns
else
Expand Down
7 changes: 7 additions & 0 deletions lib/phoenix_live_view/renderer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ defmodule Phoenix.LiveView.Renderer do
@moduledoc false

defmacro __before_compile__(env) do
preload? = Module.defines?(env.module, {:preload, 1})
render? = Module.defines?(env.module, {:render, 1})
root = Path.dirname(env.file)
filename = template_filename(env)
templates = Phoenix.Template.find_all(root, filename)

if preload? do
IO.warn(

Check warning on line 12 in lib/phoenix_live_view/renderer.ex

View workflow job for this annotation

GitHub Actions / mix test (OTP 24.3 | Elixir 1.13.4)

LiveComponent.preload/1 is deprecated (defind in Phoenix.LiveViewTest.StatefulComponent). Use LiveComponent.update_many/2 instead.

Check warning on line 12 in lib/phoenix_live_view/renderer.ex

View workflow job for this annotation

GitHub Actions / mix test (OTP 25.3 | Elixir 1.15.4)

LiveComponent.preload/1 is deprecated (defind in Phoenix.LiveViewTest.StatefulComponent). Use LiveComponent.update_many/2 instead.
"LiveComponent.preload/1 is deprecated (defind in #{inspect(env.module)}). Use LiveComponent.update_many/2 instead."
)
end

case {render?, templates} do
{true, [template | _]} ->
IO.warn(
Expand Down
60 changes: 40 additions & 20 deletions lib/phoenix_live_view/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -479,34 +479,54 @@ defmodule Phoenix.LiveView.Utils do
end

@doc """
Calls the optional `update/2` callback, otherwise update the socket directly.
Calls the optional `update/2` or `update_many/2` callback, otherwise update the socket(s) directly.
"""
def maybe_call_update!(socket, component, assigns) do
if function_exported?(component, :update, 2) do
socket =
case component.update(assigns, socket) do
{:ok, %Socket{} = socket} ->
socket

other ->
raise ArgumentError, """
invalid result returned from #{inspect(component)}.update/2.
Expected {:ok, socket}, got: #{inspect(other)}
"""
cond do
function_exported?(component, :update_many, 2) ->
[socket] = update_many!([socket], component, [assigns])
socket

function_exported?(component, :update, 2) ->
socket =
case component.update(assigns, socket) do
{:ok, %Socket{} = socket} ->
socket

other ->
raise ArgumentError, """
invalid result returned from #{inspect(component)}.update/2.
Expected {:ok, socket}, got: #{inspect(other)}
"""
end

if socket.redirected do
raise "cannot redirect socket on update. Redirect before `update/2` is called" <>
" or use `send/2` and redirect in the `handle_info/2` response"
end

if socket.redirected do
raise "cannot redirect socket on update. Redirect before `update/2` is called" <>
" or use `send/2` and redirect in the `handle_info/2` response"
end
socket

socket
else
Enum.reduce(assigns, socket, fn {k, v}, acc -> assign(acc, k, v) end)
true ->
Enum.reduce(assigns, socket, fn {k, v}, acc -> assign(acc, k, v) end)
end
end

def update_many!(sockets, component, list_of_assigns)
when is_list(list_of_assigns) and is_list(sockets) do
updated_sockets = component.update_many(list_of_assigns, sockets)
got_count = length(updated_sockets)

if got_count != length(sockets) do
raise ArgumentError,
"expected #{inspect(component)}.update_many/2 to return the same number of " <>
"sockets as the number of given sockets but got: #{inspect(got_count)}"
end

updated_sockets
end

@doc """
Signs the socket's flash into a token if it has been set.
"""
Expand Down
26 changes: 26 additions & 0 deletions test/phoenix_component/warnings_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Phoenix.WarningsTest do
use ExUnit.Case, async: true

@moduletag :after_verify
import ExUnit.CaptureIO

test "deprecated preload/1" do
warnings =
capture_io(:stderr, fn ->
defmodule DeprecatedPreloadComponent do
use Phoenix.LiveComponent

def preload(list_of_assigns) do
list_of_assigns
end

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

assert warnings =~
"LiveComponent.preload/1 is deprecated (defind in Phoenix.WarningsTest.DeprecatedPreloadComponent). Use LiveComponent.update_many/2 instead"
end
end
Loading

0 comments on commit 358068c

Please sign in to comment.