Skip to content

Commit ae8aa81

Browse files
SteffenDEjosevalim
andauthored
Disable change tracking when passing dynamic assigns (#3923)
* Disable change tracking when passing dynamic assigns When passing dynamic assigns to a component, we cannot make any assumptions about what keys are considered changed, therefore we need to disable change tracking. Consider this example: When a component is called and it receives dynamic attributes ```heex <.my_component {@action[:attrs] || %{}}>{@action.text}</.my_component> ``` defined as ```elixir attr :special, :boolean, default: false defp my_component(assigns) do ~H""" <div style={if(@special, do: "background-color: red;")}> {render_slot(@inner_block)} </div> """ end ``` Now assuming it is rendered first with `@action` as `%{text: "No red"}` and then toggling between that and `%{text: "Red", attrs: %{special: true}`: Previously, once toggled, the background color would be stuck on red, since `special` would not be considered changed, therefore the diff for the style would not be sent. * add e2e test * adjust test that assumed change tracking for dynamic assigns --------- Co-authored-by: José Valim <[email protected]>
1 parent 1e48360 commit ae8aa81

File tree

5 files changed

+101
-95
lines changed

5 files changed

+101
-95
lines changed

lib/phoenix_live_view/engine.ex

Lines changed: 21 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,14 @@ defmodule Phoenix.LiveView.Engine do
773773

774774
static_extra = extra ++ static
775775

776-
static_changed =
777-
if static_extra != [] do
776+
# Rewrite slots in static parts
777+
static = slots_to_rendered(static, vars, caller, Keyword.get(meta, :slots, []))
778+
779+
cond do
780+
# We can optimize when there are static parts and no dynamic parts.
781+
# We skip live components because they have their own tracking.
782+
static_extra != [] and dynamic == %{} and
783+
not match?({:&, _, [{:/, _, [{:live_component, _, _}, 1]}]}, fun) ->
778784
keys =
779785
for {key, value} <- static_extra,
780786
# We pass empty assigns because if this code is rendered,
@@ -792,58 +798,24 @@ defmodule Phoenix.LiveView.Engine do
792798
{_assign, keys} -> keys
793799
end)
794800

795-
quote do
796-
unquote(__MODULE__).to_component_static(
797-
unquote(keys),
798-
unquote(@assigns_var),
799-
changed,
800-
%{unquote_splicing(vars_changed_vars(vars_changed_keys))},
801-
vars_changed
802-
)
803-
end
804-
else
805-
Macro.escape(%{})
806-
end
807-
808-
static = slots_to_rendered(static, vars, caller, Keyword.get(meta, :slots, []))
801+
static_changed =
802+
quote do
803+
unquote(__MODULE__).to_component_static(
804+
unquote(keys),
805+
unquote(@assigns_var),
806+
changed,
807+
%{unquote_splicing(vars_changed_vars(vars_changed_keys))},
808+
vars_changed
809+
)
810+
end
809811

810-
cond do
811-
# We can't infer anything, so return the expression as is.
812-
static_extra == [] and dynamic == %{} ->
813-
expr
814-
815-
# Live components do not need to compute the changed because they track their own changed.
816-
match?({:&, _, [{:/, _, [{:live_component, _, _}, 1]}]}, fun) ->
817-
if dynamic == %{} do
818-
quote do: %{unquote_splicing(static)}
819-
else
820-
quote do: Map.merge(unquote(dynamic), %{unquote_splicing(static)})
821-
end
812+
quote do: %{unquote_splicing([__changed__: static_changed] ++ static)}
822813

823-
# We were actually able to find some static bits, but no dynamic.
824-
# Embed the static parts alongside the computed changed.
825814
dynamic == %{} ->
826-
quote do
827-
%{unquote_splicing([__changed__: static_changed] ++ static)}
828-
end
815+
quote do: %{unquote_splicing(static)}
829816

830-
# Merge both static and dynamic.
831817
true ->
832-
{_, keys, _} = analyze_and_return_tainted_keys(dynamic, vars, %{}, caller)
833-
keys = to_component_keys(keys)
834-
835-
quote do
836-
unquote(__MODULE__).to_component_dynamic(
837-
%{unquote_splicing(static)},
838-
unquote(dynamic),
839-
unquote(static_changed),
840-
unquote(keys),
841-
unquote(@assigns_var),
842-
changed,
843-
%{unquote_splicing(vars_changed_vars(keys))},
844-
vars_changed
845-
)
846-
end
818+
quote do: Map.merge(unquote(dynamic), %{unquote_splicing(static)})
847819
end
848820
end
849821

@@ -880,44 +852,6 @@ defmodule Phoenix.LiveView.Engine do
880852
do: {assign, changed}
881853
end
882854

883-
@doc false
884-
def to_component_dynamic(
885-
static,
886-
dynamic,
887-
_static_changed,
888-
_keys,
889-
_assigns,
890-
nil,
891-
_vars_changed_vars,
892-
nil
893-
) do
894-
merge_dynamic_static_changed(dynamic, static, nil)
895-
end
896-
897-
def to_component_dynamic(
898-
static,
899-
dynamic,
900-
static_changed,
901-
keys,
902-
assigns,
903-
changed,
904-
vars_changed_vars,
905-
vars_changed
906-
) do
907-
component_changed =
908-
if component_changed(keys, assigns, changed, vars_changed_vars, vars_changed) do
909-
Enum.reduce(dynamic, static_changed, fn {k, _}, acc -> Map.put(acc, k, true) end)
910-
else
911-
static_changed
912-
end
913-
914-
merge_dynamic_static_changed(dynamic, static, component_changed)
915-
end
916-
917-
defp merge_dynamic_static_changed(dynamic, static, changed) do
918-
dynamic |> Map.merge(static) |> Map.put(:__changed__, changed)
919-
end
920-
921855
defp component_changed(:all, _assigns, _changed, _vars_changed_vars, _vars_changed), do: true
922856

923857
defp component_changed([path], assigns, changed, vars_changed_vars, vars_changed) do
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue3919Live do
2+
use Phoenix.LiveView
3+
4+
def mount(_params, _session, socket) do
5+
{:ok, assign(socket, action: %{text: "No red"})}
6+
end
7+
8+
def handle_event("toggle_special", %{}, socket) do
9+
new_action =
10+
if socket.assigns.action[:attrs] do
11+
%{text: "No red"}
12+
else
13+
%{text: "Red", attrs: %{special: true}}
14+
end
15+
16+
{:noreply, assign(socket, action: new_action)}
17+
end
18+
19+
def render(assigns) do
20+
~H"""
21+
<.my_component {@action[:attrs] || %{}}>{@action.text}</.my_component>
22+
23+
<button phx-click="toggle_special">toggle</button>
24+
"""
25+
end
26+
27+
attr(:special, :boolean, default: false)
28+
slot(:inner_block)
29+
30+
defp my_component(assigns) do
31+
~H"""
32+
<div style={if(@special, do: "background-color: red;")}>
33+
{render_slot(@inner_block)}
34+
</div>
35+
"""
36+
end
37+
end

test/e2e/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
196196
live "/3719", Issue3719Live
197197
live "/3814", Issue3814Live
198198
live "/3819", Issue3819Live
199+
live "/3919", Issue3919Live
199200
end
200201
end
201202

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { test, expect } from "../../test-fixtures";
2+
import { syncLV } from "../../utils";
3+
4+
// https://github.com/phoenixframework/phoenix_live_view/issues/3919
5+
test("attribute defaults are properly considered as changed", async ({
6+
page,
7+
}) => {
8+
await page.goto("/issues/3919");
9+
await syncLV(page);
10+
11+
const styledDiv = page.locator("div[style]");
12+
13+
await expect(styledDiv).toContainText("No red");
14+
await expect(styledDiv).not.toHaveAttribute(
15+
"style",
16+
"background-color: red;",
17+
);
18+
await page.getByRole("button", { name: "toggle" }).click();
19+
await syncLV(page);
20+
21+
await expect(styledDiv).not.toContainText("No red");
22+
await expect(styledDiv).toHaveAttribute("style", "background-color: red;");
23+
await page.getByRole("button", { name: "toggle" }).click();
24+
await syncLV(page);
25+
26+
// bug: previously, the red background remained
27+
await expect(styledDiv).toContainText("No red");
28+
await expect(styledDiv).not.toHaveAttribute(
29+
"style",
30+
"background-color: red;",
31+
);
32+
});

test/phoenix_component/rendering_test.exs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,38 +175,40 @@ defmodule Phoenix.ComponentRenderingTest do
175175
assigns = %{foo: 1, bar: %{foo: 2}, __changed__: %{}}
176176
assert eval(~H"<.changed foo={@foo} {@bar} />") == [nil]
177177

178+
# we cannot perform any change tracking when dynamic assigns are involved
178179
assigns = %{foo: 1, bar: %{foo: 2}, __changed__: %{bar: true}}
179-
assert eval(~H"<.changed foo={@foo} {@bar} />") == [["%{foo: true}"]]
180+
assert eval(~H"<.changed foo={@foo} {@bar} />") == [["nil"]]
180181

181182
assigns = %{foo: 1, bar: %{foo: 2}, __changed__: %{foo: true}}
182-
assert eval(~H"<.changed foo={@foo} {@bar} />") == [["%{foo: true}"]]
183+
assert eval(~H"<.changed foo={@foo} {@bar} />") == [["nil"]]
183184

184185
assigns = %{foo: 1, bar: %{foo: 2}, baz: 3, __changed__: %{baz: true}}
185-
assert eval(~H"<.changed foo={@foo} {@bar} baz={@baz} />") == [["%{baz: true}"]]
186+
assert eval(~H"<.changed foo={@foo} {@bar} baz={@baz} />") == [["nil"]]
186187
end
187188

188189
test "with dynamic assigns" do
189190
assigns = %{foo: %{a: 1, b: 2}, __changed__: %{}}
190191
assert eval(~H"<.changed {@foo} />") == [nil]
191192

193+
# we cannot perform any change tracking when dynamic assigns are involved
192194
assigns = %{foo: %{a: 1, b: 2}, __changed__: %{foo: true}}
193-
assert eval(~H"<.changed {@foo} />") == [["%{a: true, b: true}"]]
195+
assert eval(~H"<.changed {@foo} />") == [["nil"]]
194196

195197
assigns = %{foo: %{a: 1, b: 2}, bar: 3, __changed__: %{bar: true}}
196-
assert eval(~H"<.changed {@foo} bar={@bar} />") == [["%{bar: true}"]]
198+
assert eval(~H"<.changed {@foo} bar={@bar} />") == [["nil"]]
197199

198200
assigns = %{foo: %{a: 1, b: 2}, bar: 3, __changed__: %{bar: true}}
199-
assert eval(~H"<.changed {%{a: 1, b: 2}} bar={@bar} />") == [["%{bar: true}"]]
201+
assert eval(~H"<.changed {%{a: 1, b: 2}} bar={@bar} />") == [["nil"]]
200202

201203
assigns = %{bar: 3, __changed__: %{bar: true}}
202204

203205
assert eval(~H"<.changed {%{a: assigns[:b], b: assigns[:a]}} bar={@bar} />") ==
204-
[["%{bar: true}"]]
206+
[["nil"]]
205207

206208
assigns = %{a: 1, b: 2, bar: 3, __changed__: %{a: true, b: true, bar: true}}
207209

208210
assert eval(~H"<.changed {%{a: assigns[:b], b: assigns[:a]}} bar={@bar} />") ==
209-
[["%{a: true, b: true, bar: true}"]]
211+
[["nil"]]
210212
end
211213

212214
defp wrapper(assigns) do

0 commit comments

Comments
 (0)