Current section

Files

Jump to
phoenix_live_view lib phoenix_live_view test diff.ex
Raw

lib/phoenix_live_view/test/diff.ex

defmodule Phoenix.LiveViewTest.Diff do
@moduledoc false
alias Phoenix.LiveViewTest.DOM
@static :s
@components :c
@stream_id :stream
@template :p
@phx_component "data-phx-component"
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)
# If we have any component, we need to get the components
# sent by the diff and remove any link between components
# statics. We cannot let those links reside in the diff
# as components can be removed at any time.
rendered =
cond do
new ->
{acc, _} =
Enum.reduce(new, {old, %{}}, fn {cid, cdiff}, {acc, cache} ->
{value, cache} = find_component(cid, cdiff, old, new, cache)
{Map.put(acc, cid, value), cache}
end)
Map.put(rendered, @components, acc)
old != %{} ->
Map.put(rendered, @components, old)
true ->
rendered
end
Map.put(rendered, :streams, streams)
end
defp find_component(cid, cdiff, old, new, cache) do
case cache do
%{^cid => cached} ->
{cached, cache}
%{} ->
{res, cache} =
case cdiff do
%{@static => cid} when is_integer(cid) and cid > 0 ->
{res, cache} = find_component(cid, new[cid], old, new, cache)
{deep_merge_diff(res, Map.delete(cdiff, @static)), cache}
%{@static => cid} when is_integer(cid) and cid < 0 ->
{deep_merge_diff(old[-cid], Map.delete(cdiff, @static)), cache}
%{} ->
{deep_merge_diff(Map.get(old, cid, %{}), cdiff), cache}
end
{res, Map.put(cache, cid, res)}
end
end
def drop_cids(rendered, cids) do
update_in(rendered[@components], &Map.drop(&1, cids))
end
defp deep_merge_diff(target, %{@template => template} = source),
do: deep_merge_diff(target, resolve_templates(Map.delete(source, @template), template))
defp deep_merge_diff(_target, %{@static => _} = source),
do: source
defp deep_merge_diff(%{} = target, %{} = source),
do: Map.merge(target, source, fn _, t, s -> deep_merge_diff(t, s) end)
defp deep_merge_diff(_target, source),
do: source
# we resolve any templates when merging, because subsequent patches can
# contain more templates that are not compatible with previous diffs
defp resolve_templates(%{@template => template} = rendered, nil) do
resolve_templates(Map.delete(rendered, @template), template)
end
defp resolve_templates(%{@static => static} = rendered, template) when is_integer(static) do
resolve_templates(Map.put(rendered, @static, Map.fetch!(template, static)), template)
end
defp resolve_templates(rendered, template) when is_map(rendered) do
Map.new(rendered, fn {k, v} -> {k, resolve_templates(v, template)} end)
end
defp resolve_templates(other, _template), do: other
def extract_streams(%{} = source, streams) when not is_struct(source) do
Enum.reduce(source, streams, fn
{@stream_id, stream}, acc -> [stream | acc]
{_key, value}, acc -> extract_streams(value, acc)
end)
end
# streams can also be in the dynamic part of the diff
def extract_streams(source, streams) when is_list(source) do
Enum.reduce(source, streams, fn el, acc -> extract_streams(el, acc) end)
end
def extract_streams(_value, acc), do: acc
# Diff rendering
def render_diff(rendered) do
rendered
|> Phoenix.LiveView.Diff.to_iodata(&add_cid_attr/2)
|> IO.iodata_to_binary()
|> DOM.parse_fragment()
|> elem(1)
end
defp add_cid_attr(cid, [head | tail]) do
head_with_cid =
Regex.replace(
~r/^(\s*(?:<!--.*?-->\s*)*)<([^\s\/>]+)/,
IO.iodata_to_binary(head),
"\\0 #{@phx_component}=\"#{to_string(cid)}\"",
global: false
)
[head_with_cid | tail]
end
end