Skip to content

Commit

Permalink
Support streams in live components
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismccord committed Feb 23, 2023
1 parent c4234a5 commit 324625f
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 26 deletions.
29 changes: 24 additions & 5 deletions lib/phoenix_live_view/diff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Phoenix.LiveView.Diff do
# handled here.
@moduledoc false

alias Phoenix.LiveView.{Utils, Rendered, Comprehension, Component}
alias Phoenix.LiveView.{Utils, Rendered, Comprehension, Component, Lifecycle}

@components :c
@static :s
Expand Down Expand Up @@ -370,7 +370,15 @@ defmodule Phoenix.LiveView.Diff do
nil = template

{_counter, diff, children, pending, components, nil} =
traverse_dynamic(socket, invoke_dynamic(rendered, changed?), children, pending, components, nil, changed?)
traverse_dynamic(
socket,
invoke_dynamic(rendered, changed?),
children,
pending,
components,
nil,
changed?
)

{diff, {fingerprint, children}, pending, components, nil}
end
Expand All @@ -385,7 +393,15 @@ defmodule Phoenix.LiveView.Diff do
changed?
) do
{_counter, diff, children, pending, components, template} =
traverse_dynamic(socket, invoke_dynamic(rendered, false), %{}, pending, components, template, changed?)
traverse_dynamic(
socket,
invoke_dynamic(rendered, false),
%{},
pending,
components,
template,
changed?
)

{diff, template} = maybe_template_static(diff, fingerprint, static, template)
{diff, {fingerprint, children}, pending, components, template}
Expand Down Expand Up @@ -508,7 +524,7 @@ defmodule Phoenix.LiveView.Diff do
end

defp inject_stacktrace([{__MODULE__, :invoke_dynamic, 2, _} | stacktrace], entry) do
[entry | Enum.drop_while(stacktrace, &elem(&1, 0) == __MODULE__)]
[entry | Enum.drop_while(stacktrace, &(elem(&1, 0) == __MODULE__))]
end

defp inject_stacktrace([head | tail], entry) do
Expand Down Expand Up @@ -708,7 +724,9 @@ defmodule Phoenix.LiveView.Diff do

socket =
if changed? or events? do
Utils.clear_changed(socket)
socket
|> Lifecycle.after_render()
|> Utils.clear_changed()
else
socket
end
Expand Down Expand Up @@ -788,6 +806,7 @@ defmodule Phoenix.LiveView.Diff do
socket.private
|> Map.take([:conn_session, :root_view])
|> Map.put(:__changed__, %{})
|> Map.put(:lifecycle, %Phoenix.LiveView.Lifecycle{})

socket =
configure_socket_for_component(socket, assigns, private, new_fingerprints())
Expand Down
8 changes: 4 additions & 4 deletions lib/phoenix_live_view/lifecycle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ defmodule Phoenix.LiveView.Lifecycle do

def attach_hook(%Socket{} = socket, id, stage, fun)
when stage in [:handle_event, :handle_info, :handle_params, :after_render] do
lifecycle = lifecycle(socket)
lifecycle = lifecycle(socket, stage)
hook = hook!(id, stage, fun)
existing = Enum.find(Map.fetch!(lifecycle, stage), &(&1.id == id))

Expand Down Expand Up @@ -84,16 +84,16 @@ defmodule Phoenix.LiveView.Lifecycle do
"""
end

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

Map.fetch!(socket.private, @lifecycle)
end

defp update_lifecycle(socket, stage, fun) do
lifecycle = lifecycle(socket)
lifecycle = lifecycle(socket, stage)
new_lifecycle = Map.update!(lifecycle, stage, fun)
put_lifecycle(socket, new_lifecycle)
end
Expand Down
34 changes: 28 additions & 6 deletions lib/phoenix_live_view/test/dom.ex
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,10 @@ defmodule Phoenix.LiveViewTest.DOM do

def merge_diff(rendered, diff) do
old = Map.get(rendered, @components, %{})
# must extract streams from diff before we pop components
streams = extract_streams(diff, [])
{new, diff} = Map.pop(diff, @components)
rendered = deep_merge_diff(rendered, diff)
streams = extract_streams(diff, [])

# If we have any component, we need to get the components
# sent by the diff and remove any link between components
Expand Down Expand Up @@ -368,9 +369,9 @@ defmodule Phoenix.LiveViewTest.DOM do
{Map.merge(in_acc, inserts), MapSet.union(deletes_acc, MapSet.new(deletes))}
end)

id = attribute(node, "id")
verify_phx_update_id!(type, id, node)
children_before = apply_phx_update_children(html_tree, id)
container_id = attribute(node, "id")
verify_phx_update_id!(type, container_id, node)
children_before = apply_phx_update_children(html_tree, container_id)
existing_ids = apply_phx_update_children_id(type, children_before)
new_ids = apply_phx_update_children_id(type, appended_children)

Expand Down Expand Up @@ -409,11 +410,15 @@ defmodule Phoenix.LiveViewTest.DOM do
new_children =
Enum.reduce(stream_inserts, children, fn {id, insert_at}, acc ->
old_index = Enum.find_index(acc, &(attribute(&1, "id") == id))
child = Enum.at(acc, old_index)
child = old_index && Enum.at(acc, old_index)
existing? = Enum.find_index(updated_existing_children, &(attribute(&1, "id") == id))
deleted? = MapSet.member?(stream_deletes, id)

cond do
# skip added children that aren't ours
parent_id(html_tree, id) != container_id ->
acc

# do not append existing child if already present, only update in place
old_index && insert_at == -1 && existing? ->
if deleted? do
Expand Down Expand Up @@ -443,7 +448,6 @@ defmodule Phoenix.LiveViewTest.DOM do
deleted? && !inserted_at
end)


{tag, attrs, new_children}

content_changed? && type == "append" ->
Expand Down Expand Up @@ -517,4 +521,22 @@ defmodule Phoenix.LiveViewTest.DOM do
defp by_id(html_tree, id) do
html_tree |> Floki.find("##{id}") |> List.first()
end

def parent_id(html_tree, child_id) do
try do
walk(html_tree, fn {tag, attrs, children} = node ->
parent_id = attribute(node, "id")

if parent_id && Enum.find(children, fn child -> attribute(child, "id") == child_id end) do
throw(parent_id)
else
{tag, attrs, children}
end
end)

nil
catch
:throw, parent_id -> parent_id
end
end
end
2 changes: 1 addition & 1 deletion test/phoenix_live_view/diff_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ defmodule Phoenix.LiveView.DiffTest do

test "slot tracking with live component inside conditional slot" do
assigns = %{socket: %Socket{}, if: true}
{socket, full_render, components} = render(conditional_slot_tracking(assigns))
{_socket, _full_render, components} = render(conditional_slot_tracking(assigns))
assert {_, _, 3} = components

assigns = %{socket: %Socket{}, if: false}
Expand Down
96 changes: 89 additions & 7 deletions test/phoenix_live_view/integrations/stream_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,117 @@ defmodule Phoenix.LiveView.StreamTest do
assert stream.inserts == []
assert stream.deletes == []

assert lv |> render() |> users_in_dom() == [{"users-1", "chris"}, {"users-2", "callan"}]
assert lv |> render() |> users_in_dom("users") == [
{"users-1", "chris"},
{"users-2", "callan"}
]

assert lv
|> element(~S|#users-1 button[phx-click="update"]|)
|> render_click()
|> users_in_dom() ==
|> users_in_dom("users") ==
[{"users-1", "updated"}, {"users-2", "callan"}]

assert_pruned_stream(lv)

assert lv
|> element(~S|#users-2 button[phx-click="move-to-first"]|)
|> render_click()
|> users_in_dom() ==
|> users_in_dom("users") ==
[{"users-2", "updated"}, {"users-1", "updated"}]

assert lv
|> element(~S|#users-2 button[phx-click="move-to-last"]|)
|> render_click()
|> users_in_dom() ==
|> users_in_dom("users") ==
[{"users-1", "updated"}, {"users-2", "updated"}]

assert lv
|> element(~S|#users-1 button[phx-click="delete"]|)
|> render_click()
|> users_in_dom() ==
|> users_in_dom("users") ==
[{"users-2", "updated"}]

assert_pruned_stream(lv)

# second stream in LiveView
assert lv |> render() |> users_in_dom("admins") == [
{"admins-1", "chris-admin"},
{"admins-2", "callan-admin"}
]

assert lv
|> element(~S|#admins-1 button[phx-click="admin-update"]|)
|> render_click()
|> users_in_dom("admins") ==
[{"admins-1", "updated"}, {"admins-2", "callan-admin"}]

assert_pruned_stream(lv)

assert lv
|> element(~S|#admins-2 button[phx-click="admin-move-to-first"]|)
|> render_click()
|> users_in_dom("admins") ==
[{"admins-2", "updated"}, {"admins-1", "updated"}]

assert lv
|> element(~S|#admins-2 button[phx-click="admin-move-to-last"]|)
|> render_click()
|> users_in_dom("admins") ==
[{"admins-1", "updated"}, {"admins-2", "updated"}]

assert lv
|> element(~S|#admins-1 button[phx-click="admin-delete"]|)
|> render_click()
|> users_in_dom("admins") ==
[{"admins-2", "updated"}]
end

describe "within live component" do
test "stream operations", %{conn: conn} do
{:ok, lv, _html} = live(conn, "/stream")

assert lv |> render() |> users_in_dom("c_users") == [
{"c_users-1", "chris"},
{"c_users-2", "callan"}
]

assert lv
|> element(~S|#c_users-1 button[phx-click="update"]|)
|> render_click()
|> users_in_dom("c_users") ==
[{"c_users-1", "updated"}, {"c_users-2", "callan"}]

assert_pruned_stream(lv)

assert lv
|> element(~S|#c_users-2 button[phx-click="move-to-first"]|)
|> render_click()
|> users_in_dom("c_users") ==
[{"c_users-2", "updated"}, {"c_users-1", "updated"}]

assert lv
|> element(~S|#c_users-2 button[phx-click="move-to-last"]|)
|> render_click()
|> users_in_dom("c_users") ==
[{"c_users-1", "updated"}, {"c_users-2", "updated"}]

assert lv
|> element(~S|#c_users-1 button[phx-click="delete"]|)
|> render_click()
|> users_in_dom("c_users") ==
[{"c_users-2", "updated"}]

Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.StreamComponent,
id: "stream-component",
send_assigns_to: self()
)

assert_receive {:assigns, %{streams: streams}}
assert streams.c_users.inserts == []
assert streams.c_users.deletes == []
assert_pruned_stream(lv)
end
end

defp assert_pruned_stream(lv) do
Expand All @@ -64,10 +146,10 @@ defmodule Phoenix.LiveView.StreamTest do
assert stream.deletes == []
end

defp users_in_dom(html) do
defp users_in_dom(html, parent_id) do
html
|> DOM.parse()
|> DOM.all("#users > *")
|> DOM.all("##{parent_id} > *")
|> Enum.map(fn {_tag, [{"id", id}], [text | _children]} ->
{id, String.trim(text)}
end)
Expand Down
Loading

0 comments on commit 324625f

Please sign in to comment.