Packages
phoenix_live_view
0.6.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.3
1.2.0-rc.2
1.2.0-rc.1
1.2.0-rc.0
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.23
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc.4
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
retired
1.0.7
1.0.6
retired
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.14
0.17.13
0.17.12
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.6.0-dev
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
Rich, real-time user experiences with server-rendered HTML
Current section
Files
Jump to
Current section
Files
lib/phoenix_live_view/test/dom.ex
defmodule Phoenix.LiveViewTest.DOM do
@moduledoc false
@phx_component "data-phx-component"
@static :s
@dynamics :d
@components :c
def by_id!(html_tree, id) do
by_id(html_tree, id) || raise ArgumentError, "could not find ID #{inspect(id)} in the DOM"
end
def cid_by_id(rendered, id) do
rendered
|> render_diff()
|> by_id!(id)
|> all_attributes(@phx_component)
|> case do
[cid] -> String.to_integer(cid)
[] -> nil
end
end
def all(html_tree, selector), do: Floki.find(html_tree, selector)
def parse(html) do
{:ok, parsed} = Floki.parse_document(html)
parsed
end
def attrs({_tag, attrs, _children}), do: Enum.into(attrs, %{})
def attrs({_tag, attrs, _children}, key), do: Enum.into(attrs, %{})[key]
def all_attributes(html_tree, name), do: Floki.attribute(html_tree, name)
def to_html(html_tree), do: Floki.raw_html(html_tree)
def filter_out(html_tree, selector), do: Floki.filter_out(html_tree, selector)
def child_nodes({_, _, children}), do: children
def inner_html(html, id), do: html |> by_id!(id) |> child_nodes()
def outer_html(html, id), do: html |> by_id!(id)
def find_static_views(html) do
html
|> all("[data-phx-static]")
|> Enum.into(%{}, fn node ->
attrs = attrs(node)
{attrs["id"], attrs["data-phx-static"]}
end)
end
def find_live_views(html) do
html
|> all("[data-phx-session]")
|> Enum.map(fn node ->
attrs = attrs(node)
static =
cond do
attrs["data-phx-static"] in [nil, ""] -> nil
true -> attrs["data-phx-static"]
end
{attrs["id"], attrs["data-phx-session"], static}
end)
end
def deep_merge(target, source) do
Map.merge(target, source, fn
_, %{} = target, %{} = source -> deep_merge(target, source)
_, _target, source -> source
end)
end
# Diff rendering
def render_diff(rendered) do
render_diff(rendered, Map.get(rendered, @components, %{}))
end
def render_diff(rendered, components) do
rendered
|> to_output_buffer(components, [])
|> Enum.reverse()
|> IO.iodata_to_binary()
|> parse()
|> List.wrap()
end
# for comprehension
defp to_output_buffer(%{@dynamics => for_dynamics, @static => statics}, components, acc) do
Enum.reduce(for_dynamics, acc, fn dynamics, acc ->
dynamics
|> Enum.with_index()
|> Enum.into(%{@static => statics}, fn {val, key} -> {key, val} end)
|> to_output_buffer(components, acc)
end)
end
defp to_output_buffer(%{@static => [head | tail]} = rendered, components, acc) do
tail
|> Enum.with_index(0)
|> Enum.reduce([head | acc], fn {static, index}, acc ->
[static | dynamic_to_buffer(rendered[index], components, acc)]
end)
end
defp dynamic_to_buffer(%{} = rendered, components, acc) do
to_output_buffer(rendered, components, []) ++ acc
end
defp dynamic_to_buffer(str, _components, acc) when is_binary(str), do: [str | acc]
defp dynamic_to_buffer(cid, components, acc) when is_integer(cid) do
html_with_cids =
components
|> Map.fetch!(cid)
|> render_diff(components)
|> Enum.map(walk_fun(&inject_cid_attr(&1, cid)))
|> to_html()
[html_with_cids | acc]
end
defp inject_cid_attr({tag, attrs, children}, cid) do
{tag, attrs ++ [{@phx_component, to_string(cid)}], children}
end
# Patching
def patch_id(id, html, inner_html) do
cids_before = find_component_ids(id, html)
phx_update_tree =
walk(inner_html, fn node ->
apply_phx_update(attrs(node, "phx-update"), html, node)
end)
new_html =
walk(html, fn {tag, attrs, children} = node ->
if attrs(node, "id") == id do
{tag, attrs, phx_update_tree}
else
{tag, attrs, children}
end
end)
cids_after = find_component_ids(id, new_html)
deleted_cids = for cid <- cids_before -- cids_after, do: String.to_integer(cid)
deleted_ids =
html
|> all(Enum.join(Enum.map(deleted_cids, &"[#{@phx_component}=\"#{&1}\"]"), ", "))
|> all_attributes("id")
{new_html, deleted_cids, deleted_ids}
end
defp walk(html_tree, fun) when is_function(fun, 1) do
Floki.traverse_and_update(html_tree, walk_fun(fun))
end
defp walk_fun(fun) when is_function(fun, 1) do
fn
{:pi, _, _} = xml -> xml
{:comment, _children} = comment -> comment
{:doctype, _, _, _} = doctype -> doctype
{_tag, _attrs, _children} = node -> fun.(node)
end
end
defp find_component_ids(id, html) do
html
|> by_id!(id)
|> all("[#{@phx_component}]")
|> all_attributes(@phx_component)
end
defp apply_phx_update(type, html, {tag, attrs, appended_children} = node)
when type in ["append", "prepend"] do
children_before = phx_update_children(html, attrs(node, "id"))
existing_ids = all_attributes(children_before, "id")
new_ids = all_attributes(appended_children, "id")
content_changed? = new_ids !== existing_ids
dup_ids =
if content_changed? && new_ids do
Enum.filter(new_ids, fn id -> id in existing_ids end)
else
[]
end
{updated_existing_children, updated_appended} =
Enum.reduce(dup_ids, {children_before, appended_children}, fn dup_id, {before, appended} ->
patched_before =
walk(before, fn {tag, attrs, _} = node ->
cond do
attrs(node, "id") == dup_id -> {tag, attrs, inner_html(appended, dup_id)}
true -> node
end
end)
{patched_before, filter_out(appended, "##{dup_id}")}
end)
cond do
content_changed? && type == "append" ->
{tag, attrs, updated_existing_children ++ updated_appended}
content_changed? && type == "prepend" ->
{tag, attrs, updated_appended ++ updated_existing_children}
!content_changed? ->
{tag, attrs, updated_appended}
end
end
defp apply_phx_update(type, _state, {tag, attrs, children})
when type in [nil, "replace", "ignore"] do
{tag, attrs, children}
end
defp apply_phx_update(other, _state, {_tag, _attrs, _children}) do
raise ArgumentError, """
invalid phx-update value #{inspect(other)}.
Expected one of "replace", "append", "prepend", "ignore"
"""
end
def phx_update_children(html, id) do
case by_id(html, id) do
{_, _, children_before} -> children_before
nil -> raise ArgumentError, "phx-update append/prepend containers require an ID"
end
end
defp by_id(html_tree, ids) when is_list(ids) do
selector = Enum.join(Enum.map(ids, fn id -> "##{id}" end), " ")
case Floki.find(html_tree, selector) do
[node | _] -> node
[] -> nil
end
end
defp by_id(html_tree, id) do
case Floki.find(html_tree, "##{id}") do
[node | _] -> node
[] -> nil
end
end
end