Skip to content

Commit

Permalink
clear private HOOK_ID as soon as hook is destroyed
Browse files Browse the repository at this point in the history
Fixes #3496.

When navigating from a sticky LiveView to another page that contains
a hook on an element with the same id, morphdom would merge the old
element into the new one and still find the old HOOK_ID in the element's
DOM.private data, leading to the new hook not initializing properly.
  • Loading branch information
SteffenDE committed Nov 11, 2024
1 parent 2869e28 commit 76d405b
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 1 deletion.
5 changes: 4 additions & 1 deletion assets/js/phoenix_live_view/view_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export default class ViewHook {
__mounted(){ this.mounted && this.mounted() }
__updated(){ this.updated && this.updated() }
__beforeUpdate(){ this.beforeUpdate && this.beforeUpdate() }
__destroyed(){ this.destroyed && this.destroyed() }
__destroyed(){
this.destroyed && this.destroyed()
DOM.deletePrivate(this.el, HOOK_ID) // https://github.com/phoenixframework/phoenix_live_view/issues/3496
}
__reconnected(){
if(this.__isDisconnected){
this.__isDisconnected = false
Expand Down
File renamed without changes.
111 changes: 111 additions & 0 deletions test/e2e/support/issues/issue_3496.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
defmodule Phoenix.LiveViewTest.E2E.Issue3496.ALive do
# https://github.com/phoenixframework/phoenix_live_view/issues/3496

use Phoenix.LiveView

alias Phoenix.LiveView.JS

def base(assigns) do
~H"""
<meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />
<script src="/assets/phoenix/phoenix.min.js">
</script>
<script type="module">
import {LiveSocket} from "/assets/phoenix_live_view/phoenix_live_view.esm.js"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new LiveSocket("/live", window.Phoenix.Socket, {params: {_csrf_token: csrfToken}, hooks: {
MyHook: {
mounted() {
console.log("Hook mounted!")
}
}
}})
liveSocket.connect()
window.liveSocket = liveSocket
</script>
<style>
* { font-size: 1.1em; }
</style>
"""
end

def with_sticky(assigns) do
~H"""
<.base />
<div>
<%= @inner_content %>
</div>
<%= live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3496.StickyLive,
id: "sticky",
sticky: true
) %>
"""
end

def without_sticky(assigns) do
~H"""
<.base />
<div>
<%= @inner_content %>
</div>
"""
end

def mount(_params, _session, socket) do
{:ok, socket, layout: {__MODULE__, :with_sticky}}
end

def render(assigns) do
~H"""
<h1>Page A</h1>
<.link navigate="/issues/3496/b">Go to page B</.link>
"""
end
end

defmodule Phoenix.LiveViewTest.E2E.Issue3496.BLive do
use Phoenix.LiveView

alias Phoenix.LiveView.JS

def mount(_params, _session, socket) do
{:ok, socket, layout: {Phoenix.LiveViewTest.E2E.Issue3496.ALive, :without_sticky}}
end

def render(assigns) do
~H"""
<h1>Page B</h1>
<Phoenix.LiveViewTest.E2E.Issue3496.MyComponent.my_component />
"""
end
end

defmodule Phoenix.LiveViewTest.E2E.Issue3496.StickyLive do
use Phoenix.LiveView

def mount(_params, _session, socket) do
{:ok, socket, layout: false}
end

def render(assigns) do
~H"""
<div>
<Phoenix.LiveViewTest.E2E.Issue3496.MyComponent.my_component />
</div>
"""
end
end

defmodule Phoenix.LiveViewTest.E2E.Issue3496.MyComponent do
use Phoenix.Component

def my_component(assigns) do
~H"""
<div id="my-component" phx-hook="MyHook"></div>
"""
end
end
2 changes: 2 additions & 0 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
live "/3194/other", Issue3194Live.OtherLive
live "/3378", Issue3378.HomeLive
live "/3448", Issue3448Live
live "/3496/a", Issue3496.ALive
live "/3496/b", Issue3496.BLive
end
end

Expand Down
21 changes: 21 additions & 0 deletions test/e2e/tests/issues/3496.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { test, expect } = require("../../test-fixtures");
const { syncLV } = require("../../utils");

// https://github.com/phoenixframework/phoenix_live_view/issues/3496
test("hook is initialized properly when reusing id between sticky and non sticky LiveViews", async ({ page }) => {
const logs = [];
page.on("console", (e) => logs.push(e.text()));
const errors = [];
page.on("pageerror", (err) => errors.push(err));

await page.goto("/issues/3496/a");
await syncLV(page);

await page.getByRole("link", { name: "Go to page B" }).click();
await syncLV(page);

expect(logs.filter(e => e.includes("Hook mounted!"))).toHaveLength(2);
expect(logs).not.toEqual(expect.arrayContaining([expect.stringMatching("no hook found for custom element")]));
// no uncaught exceptions
expect(errors).toEqual([]);
});

0 comments on commit 76d405b

Please sign in to comment.