Skip to content

Commit a36de10

Browse files
committed
recursively undo cloned trees (#3690)
* recursively undo cloned trees Fixes: #3684. Relates to: #3652 The issue was that when we locked a form with an input inside where the input changed, the form was cloned and then later the input inside was cloned as well, stored inside the already cloned parent. When undoing, previously the simple patch copied over the PHX_PRIVATE of the cloned elements, but with the full DOMPatch, the privates of the source element were copied, discarding the clone of the input. With this change, we check if we are undoing locks and manually copy over nested clones to ensure that they can be correctly applied afterwards. * lint
1 parent ca5d242 commit a36de10

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

assets/js/phoenix_live_view/dom_patch.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,11 @@ export default class DOMPatch {
245245
return false
246246
}
247247

248-
// input handling
248+
// if we are undoing a lock, copy potentially nested clones over
249+
if(this.undoRef && DOM.private(toEl, PHX_REF_LOCK)){
250+
DOM.putPrivate(fromEl, PHX_REF_LOCK, DOM.private(toEl, PHX_REF_LOCK))
251+
}
252+
// now copy regular DOM.private data
249253
DOM.copyPrivates(toEl, fromEl)
250254

251255
// skip patching focused inputs unless focus is a select that has changed options

test/e2e/support/issues/issue_3684.ex

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue3684Live do
2+
# https://github.com/phoenixframework/phoenix_live_view/issues/3684
3+
use Phoenix.LiveView
4+
5+
defmodule BadgeForm do
6+
use Phoenix.LiveComponent
7+
8+
def mount(socket) do
9+
socket =
10+
socket
11+
|> assign(:type, :huey)
12+
13+
{:ok, socket}
14+
end
15+
16+
def update(assigns, socket) do
17+
socket =
18+
socket
19+
|> assign(:form, assigns.form)
20+
21+
{:ok, socket}
22+
end
23+
24+
def render(assigns) do
25+
~H"""
26+
<div>
27+
<.form
28+
for={@form}
29+
id="foo"
30+
class="max-w-lg p-8 flex flex-col gap-4"
31+
phx-change="change"
32+
phx-submit="submit"
33+
>
34+
<.radios type={@type} form={@form} myself={@myself} />
35+
</.form>
36+
</div>
37+
"""
38+
end
39+
40+
defp radios(assigns) do
41+
~H"""
42+
<fieldset>
43+
<legend>Radio example:</legend>
44+
<%= for type <- [:huey, :dewey] do %>
45+
<div phx-click="change-type" phx-value-type={type} phx-target={@myself}>
46+
<input type="radio" id={type} name="type" value={type} checked={@type == type} />
47+
<label for={type}>{type}</label>
48+
</div>
49+
<% end %>
50+
</fieldset>
51+
"""
52+
end
53+
54+
def handle_event("change-type", %{"type" => type}, socket) do
55+
type = String.to_existing_atom(type)
56+
socket = assign(socket, :type, type)
57+
{:noreply, socket}
58+
end
59+
end
60+
61+
defp changeset(params) do
62+
data = %{}
63+
64+
types = %{
65+
type: :string
66+
}
67+
68+
{data, types}
69+
|> Ecto.Changeset.cast(params, Map.keys(types))
70+
|> Ecto.Changeset.validate_required(:type)
71+
end
72+
73+
def mount(_params, _session, socket) do
74+
{:ok, assign(socket, form: to_form(changeset(%{}), as: :foo), payload: nil)}
75+
end
76+
77+
def render(assigns) do
78+
~H"""
79+
<.live_component id="badge_form" module={__MODULE__.BadgeForm} action={@live_action} form={@form} />
80+
"""
81+
end
82+
83+
def handle_event("change", params, socket) do
84+
{:noreply, socket}
85+
end
86+
87+
def handle_event("submit", params, socket) do
88+
{:noreply, socket}
89+
end
90+
end

test/e2e/test_helper.exs

+1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
162162
live "/3651", Issue3651Live
163163
live "/3656", Issue3656Live
164164
live "/3658", Issue3658Live
165+
live "/3684", Issue3684Live
165166
end
166167
end
167168

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

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const {test, expect} = require("../../test-fixtures")
2+
const {syncLV} = require("../../utils")
3+
4+
// https://github.com/phoenixframework/phoenix_live_view/issues/3684
5+
test("nested clones are correctly applied", async ({page}) => {
6+
await page.goto("/issues/3684")
7+
await syncLV(page)
8+
9+
await expect(page.locator("#dewey")).not.toHaveAttribute("checked")
10+
11+
await page.locator("#dewey").click()
12+
await syncLV(page)
13+
14+
await expect(page.locator("#dewey")).toHaveAttribute("checked")
15+
})

0 commit comments

Comments
 (0)