Skip to content

Commit 9a867b8

Browse files
authored
ignore events for elements that are not connected (#4074)
Closes #4066.
1 parent 8a50f0a commit 9a867b8

File tree

4 files changed

+86
-1
lines changed

4 files changed

+86
-1
lines changed

assets/js/phoenix_live_view/live_socket.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,11 @@ export default class LiveSocket {
531531
// in that case we DO NOT want to fallback to the main element
532532
view = this.getViewByEl(viewEl);
533533
} else {
534+
if (!childEl.isConnected) {
535+
// if the element is not part of the DOM any more
536+
// there's no owner and we should not do fall back
537+
return null;
538+
}
534539
view = this.main;
535540
}
536541
return view && callback ? callback(view) : view;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue4066Live do
2+
use Phoenix.LiveView, layout: {__MODULE__, :live}
3+
4+
alias Phoenix.LiveViewTest.E2E.Issue4066Live
5+
6+
def mount(params, _session, socket) do
7+
{:ok, assign(socket, delay: params["delay"] || 3000, render_lc: true)}
8+
end
9+
10+
def render(assigns) do
11+
~H"""
12+
<p id="render-time">{DateTime.utc_now()}</p>
13+
<button phx-click="toggle">Toggle</button>
14+
<.live_component :if={@render_lc} id="foo" delay={@delay} module={Issue4066Live.LiveComponent} />
15+
"""
16+
end
17+
18+
def handle_event("toggle", _params, socket) do
19+
{:noreply, assign(socket, :render_lc, !socket.assigns.render_lc)}
20+
end
21+
end
22+
23+
defmodule Phoenix.LiveViewTest.E2E.Issue4066Live.LiveComponent do
24+
use Phoenix.LiveComponent
25+
26+
def render(assigns) do
27+
~H"""
28+
<script :type={Phoenix.LiveView.ColocatedHook} name=".MyHook">
29+
export default {
30+
mounted() {
31+
this.el.addEventListener("input", () => {
32+
setTimeout(() => {
33+
this.pushEventTo(this.el, "do-something", { value: 100 })
34+
this.liveSocket.js().setAttribute(document.body, "data-pushed", "yes");
35+
}, parseInt(this.el.dataset.delay));
36+
})
37+
}
38+
}
39+
</script>
40+
<input phx-hook=".MyHook" data-delay={@delay} target={@myself} id={@id} />
41+
"""
42+
end
43+
44+
def handle_event("do-something", %{"value" => value}, socket) do
45+
{:noreply, socket}
46+
end
47+
end

test/e2e/test_helper.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ defmodule Phoenix.LiveViewTest.E2E.Layout do
4242
{assigns[:pre_script]}
4343
<script type="module">
4444
import {LiveSocket} from "/assets/phoenix_live_view/phoenix_live_view.esm.js"
45+
import {hooks as colocatedHooks} from "/assets/colocated/index.js";
4546
4647
let Hooks = {}
4748
Hooks.FormHook = {
@@ -57,7 +58,7 @@ defmodule Phoenix.LiveViewTest.E2E.Layout do
5758
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
5859
let liveSocket = new LiveSocket("/live", window.Phoenix.Socket, {
5960
params: {_csrf_token: csrfToken},
60-
hooks: {...Hooks, ...window.hooks}
61+
hooks: {...Hooks, ...window.hooks, ...colocatedHooks}
6162
})
6263
liveSocket.connect()
6364
window.liveSocket = liveSocket
@@ -201,6 +202,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
201202
live "/3953", Issue3953Live
202203
live "/3979", Issue3979Live
203204
live "/4027", Issue4027Live
205+
live "/4066", Issue4066Live
204206
end
205207
end
206208

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { test, expect } from "../../test-fixtures";
2+
import { syncLV } from "../../utils";
3+
4+
// https://github.com/phoenixframework/phoenix_live_view/issues/4066
5+
test("events for disconnected elements are ignored", async ({ page }) => {
6+
// The test triggers an input event that triggers an event after a delay
7+
// and before the delay fires, the element is removed by a button press.
8+
// Previously, the event would bubble to the parent, crashing the LiveView.
9+
await page.goto("/issues/4066?delay=100");
10+
await syncLV(page);
11+
12+
const renderTime = await page
13+
.locator("#render-time")
14+
.evaluate((el) => el.innerText);
15+
16+
await page.locator("input").fill("123");
17+
await page.locator("button").click();
18+
await syncLV(page);
19+
await expect(page.locator("input")).toBeHidden();
20+
21+
// The hook sets this attribute when the delay fires - we should not crash here
22+
await expect(page.locator("body")).toHaveAttribute("data-pushed", "yes");
23+
24+
// We can show the input again
25+
await page.locator("button").click();
26+
await syncLV(page);
27+
await expect(page.locator("input")).toBeVisible();
28+
29+
// LiveView did not remount
30+
await expect(page.locator("#render-time")).toHaveText(renderTime);
31+
});

0 commit comments

Comments
 (0)