Skip to content

Commit 859f59d

Browse files
authored
Ensure __changed__ is reset when using dynamic assigns (#3933)
To fully disable change tracking, we need to force `__changed__` to nil. Closes #3931.
1 parent 8d6724c commit 859f59d

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

lib/phoenix_live_view/engine.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,11 @@ defmodule Phoenix.LiveView.Engine do
815815
quote do: %{unquote_splicing(static)}
816816

817817
true ->
818-
quote do: Map.merge(unquote(dynamic), %{unquote_splicing(static)})
818+
# we must disable change tracking when there is a non empty dynamic part
819+
# (for example `<.my_component {assigns}>`) for anything inside the component;
820+
# in case the parent assigns already contain a `__changed__` key, we must reset
821+
# it to `nil` to do so
822+
quote do: Map.merge(unquote(dynamic), %{unquote_splicing([__changed__: nil] ++ static)})
819823
end
820824
end
821825

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue3931Live do
2+
use Phoenix.LiveView
3+
4+
def mount(_params, _session, socket) do
5+
socket =
6+
socket
7+
|> assign_async(:slow_data, fn ->
8+
Process.sleep(100)
9+
{:ok, %{slow_data: "This was loaded asynchronously!"}}
10+
end)
11+
12+
{:ok, socket}
13+
end
14+
15+
def layout(assigns) do
16+
~H"""
17+
<div class="max-w-4xl mx-auto p-8">
18+
{render_slot(@inner_block)}
19+
</div>
20+
"""
21+
end
22+
23+
def render(assigns) do
24+
~H"""
25+
<.layout {assigns}>
26+
<.async_result :let={data} assign={@slow_data}>
27+
<:loading>
28+
<div id="async" class="flex items-center space-x-3">
29+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
30+
<p class="text-gray-600">Loading data...</p>
31+
</div>
32+
</:loading>
33+
34+
<div id="async" class="space-y-3">
35+
{data}
36+
</div>
37+
</.async_result>
38+
</.layout>
39+
"""
40+
end
41+
end

test/e2e/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
197197
live "/3814", Issue3814Live
198198
live "/3819", Issue3819Live
199199
live "/3919", Issue3919Live
200+
live "/3931", Issue3931Live
200201
end
201202
end
202203

test/e2e/tests/issues/3931.spec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { test, expect } from "../../test-fixtures";
2+
import { syncLV } from "../../utils";
3+
4+
// https://github.com/phoenixframework/phoenix_live_view/issues/3931
5+
test("dynamic attributes reset __changed__ and properly re-render", async ({
6+
page,
7+
}) => {
8+
let webSocketEvents = [];
9+
page.on("websocket", (ws) => {
10+
ws.on("framesent", (event) =>
11+
webSocketEvents.push({ type: "sent", payload: event.payload }),
12+
);
13+
ws.on("framereceived", (event) =>
14+
webSocketEvents.push({ type: "received", payload: event.payload }),
15+
);
16+
ws.on("close", () => webSocketEvents.push({ type: "close" }));
17+
});
18+
19+
await page.goto("/issues/3931");
20+
await syncLV(page);
21+
22+
// it should be updated asynchronously
23+
await expect(page.locator("#async")).toContainText(
24+
"This was loaded asynchronously!",
25+
);
26+
27+
expect(webSocketEvents).toEqual(
28+
expect.arrayContaining([
29+
{ type: "sent", payload: expect.stringContaining("phx_join") },
30+
{ type: "received", payload: expect.stringContaining("phx_reply") },
31+
{ type: "received", payload: expect.stringContaining("diff") },
32+
]),
33+
);
34+
});

0 commit comments

Comments
 (0)