Skip to content
This repository was archived by the owner on Feb 10, 2026. It is now read-only.
Closed
4 changes: 4 additions & 0 deletions lib/live_view_native/template/engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ defmodule LiveViewNative.Template.Engine do
@impl true
defdelegate annotate_caller(file, line), to: Phoenix.LiveView.HTMLEngine

@doc false
@impl true
defdelegate annotate_slot(name, tag_meta, close_meta, caller), to: Phoenix.LiveView.HTMLEngine

@doc false
@impl true
def classify_type(":inner_block"), do: {:error, "the slot name :inner_block is reserved"}
Expand Down
71 changes: 71 additions & 0 deletions lib/live_view_native/test/view_tree.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ defmodule LiveViewNativeTest.ViewTree do
@phx_component "data-phx-component"
@static :s
@components :c
@template :p
@keyed :k
@keyed_count :kc
@stream_id :stream

def ensure_loaded! do
Expand Down Expand Up @@ -284,6 +287,38 @@ defmodule LiveViewNativeTest.ViewTree do
update_in(rendered[@components], &Map.drop(&1, cids))
end

# We resolve any templates when merging, because subsequent patches can
# contain more templates that are not compatible with previous diffs.
# This prevents template position collisions from breaking rendering.
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, %{@keyed => source_keyed} = source) when is_map(target) do
target_keyed = target[@keyed]

merged_keyed =
case source_keyed[@keyed_count] do
0 ->
%{@keyed_count => 0}

count ->
for pos <- 0..(count - 1), into: %{@keyed_count => count} do
value =
case source_keyed[pos] do
nil -> target_keyed[pos]
value when is_number(value) -> target_keyed[value]
value when is_map(value) -> deep_merge_diff(target_keyed[pos], value)
[old_pos, value] -> deep_merge_diff(target_keyed[old_pos], value)
end

{pos, value}
end
end

merged = deep_merge_diff(Map.delete(target, @keyed), Map.delete(source, @keyed))
Map.put(merged, @keyed, merged_keyed)
end

defp deep_merge_diff(_target, %{@static => _} = source),
do: source

Expand All @@ -293,6 +328,42 @@ defmodule LiveViewNativeTest.ViewTree do
defp deep_merge_diff(_target, source),
do: source

# Template resolution helpers - convert template position references to literal static parts
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(%{@keyed => keyed} = rendered, template) do
keyed =
case keyed[@keyed_count] do
0 ->
keyed

count ->
for pos <- 0..(count - 1), reduce: keyed do
acc ->
case keyed[pos] do
nil -> acc
value -> Map.put(acc, pos, resolve_templates(value, template))
end
end
end

rendered
|> Map.put(@keyed, keyed)
|> Map.delete(@template)
end

defp resolve_templates(rendered, template) when is_map(rendered) and not is_struct(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]
Expand Down
2 changes: 1 addition & 1 deletion lib/live_view_native_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ defmodule LiveViewNativeTest do
end

defp rendered_to_diff_string(rendered, socket) do
{_, diff, _} = Diff.render(socket, rendered, Diff.new_components())
{diff, _, _} = Diff.render(socket, rendered, Diff.new_fingerprints(), Diff.new_components())
diff |> Diff.to_iodata() |> IO.iodata_to_binary()
end

Expand Down
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ defmodule LiveViewNative.MixProject do

defp deps do
[
{:phoenix, "~> 1.7.21"},
{:phoenix, "~> 1.8.0"},
{:phoenix_view, "~> 2.0"},
{:phoenix_live_view, "~> 1.0.18"},
{:phoenix_live_view, "~> 1.1.0"},
{:phoenix_live_reload, "~> 1.4", only: :test},
{:phoenix_template, "~> 1.0.4"},
{:phoenix_html, "~> 3.3 or ~> 4.0 or ~> 4.1"},
{:decimal, "~> 2.3", only: :test},
{:floki, ">= 0.30.0", only: :test},
{:lazy_html, ">= 0.1.0", only: :test},
{:gettext, "~> 0.24", only: :test},
{:plug, "~> 1.15"},
{:jason, "~> 1.2"},
Expand Down
Loading