Skip to content

Commit 0abd80b

Browse files
committed
fix phx-remove and phx-click-loading in sticky LVs (#3659)
Fixes #3656 Fixes #3658
1 parent 485c349 commit 0abd80b

File tree

7 files changed

+148
-12
lines changed

7 files changed

+148
-12
lines changed

assets/js/phoenix_live_view/dom_patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ export default class DOMPatch {
433433
transitionPendingRemoves(){
434434
let {pendingRemoves, liveSocket} = this
435435
if(pendingRemoves.length > 0){
436-
liveSocket.transitionRemoves(pendingRemoves, false, () => {
436+
liveSocket.transitionRemoves(pendingRemoves, () => {
437437
pendingRemoves.forEach(el => {
438438
let child = DOM.firstPhxChild(el)
439439
if(child){ liveSocket.destroyViewByEl(child) }

assets/js/phoenix_live_view/live_socket.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -393,22 +393,26 @@ export default class LiveSocket {
393393
}
394394

395395
replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)){
396-
let liveReferer = this.currentLocation.href
396+
const liveReferer = this.currentLocation.href
397397
this.outgoingMainEl = this.outgoingMainEl || this.main.el
398-
let removeEls = DOM.all(this.outgoingMainEl, `[${this.binding("remove")}]`)
399-
let newMainEl = DOM.cloneNode(this.outgoingMainEl, "")
398+
399+
const stickies = DOM.findPhxSticky(document) || []
400+
const removeEls = DOM.all(this.outgoingMainEl, `[${this.binding("remove")}]`)
401+
.filter(el => !DOM.isChildOfAny(el, stickies))
402+
403+
const newMainEl = DOM.cloneNode(this.outgoingMainEl, "")
400404
this.main.showLoader(this.loaderTimeout)
401405
this.main.destroy()
402406

403407
this.main = this.newRootView(newMainEl, flash, liveReferer)
404408
this.main.setRedirect(href)
405-
this.transitionRemoves(removeEls, true)
409+
this.transitionRemoves(removeEls)
406410
this.main.join((joinCount, onDone) => {
407411
if(joinCount === 1 && this.commitPendingLink(linkRef)){
408412
this.requestDOMUpdate(() => {
409413
// remove phx-remove els right before we replace the main element
410414
removeEls.forEach(el => el.remove())
411-
DOM.findPhxSticky(document).forEach(el => newMainEl.appendChild(el))
415+
stickies.forEach(el => newMainEl.appendChild(el))
412416
this.outgoingMainEl.replaceWith(newMainEl)
413417
this.outgoingMainEl = null
414418
callback && callback(linkRef)
@@ -418,12 +422,8 @@ export default class LiveSocket {
418422
})
419423
}
420424

421-
transitionRemoves(elements, skipSticky, callback){
425+
transitionRemoves(elements, callback){
422426
let removeAttr = this.binding("remove")
423-
if(skipSticky){
424-
const stickies = DOM.findPhxSticky(document) || []
425-
elements = elements.filter(el => !DOM.isChildOfAny(el, stickies))
426-
}
427427
let silenceEvents = (e) => {
428428
e.preventDefault()
429429
e.stopImmediatePropagation()
@@ -800,7 +800,8 @@ export default class LiveSocket {
800800
}
801801

802802
historyRedirect(e, href, linkState, flash, targetEl){
803-
if(targetEl && e.isTrusted && e.type !== "popstate"){ targetEl.classList.add("phx-click-loading") }
803+
const clickLoading = targetEl && e.isTrusted && e.type !== "popstate"
804+
if(clickLoading){ targetEl.classList.add("phx-click-loading") }
804805
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href, flash) }
805806

806807
// convert to full href if only path prefix
@@ -829,6 +830,9 @@ export default class LiveSocket {
829830
DOM.dispatchEvent(window, "phx:navigate", {detail: {href, patch: false, pop: false, direction: "forward"}})
830831
this.registerNewLocation(window.location)
831832
}
833+
// explicitly undo click-loading class
834+
// (in case it originated in a sticky live view, otherwise it would be removed anyway)
835+
if(clickLoading){ targetEl.classList.remove("phx-click-loading") }
832836
done()
833837
})
834838
})
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue3656Live do
2+
# https://github.com/phoenixframework/phoenix_live_view/issues/3656
3+
4+
use Phoenix.LiveView
5+
6+
alias Phoenix.LiveView.JS
7+
8+
def mount(_params, _session, socket) do
9+
{:ok, socket}
10+
end
11+
12+
def render(assigns) do
13+
~H"""
14+
<style>
15+
* { font-size: 1.1em }
16+
nav { margin-top: 1em }
17+
nav a { padding: 8px 16px; border: 1px solid black; text-decoration: none }
18+
nav a:visited { color: inherit }
19+
nav a.active { border: 3px solid green }
20+
nav a.phx-click-loading { animation: pulsate 2s infinite }
21+
@keyframes pulsate {
22+
0% {
23+
background-color: white;
24+
}
25+
50% {
26+
background-color: red;
27+
}
28+
100% {
29+
background-color: white;
30+
}
31+
}
32+
</style>
33+
34+
{live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3656Live.Sticky,
35+
id: "sticky",
36+
sticky: true
37+
)}
38+
"""
39+
end
40+
end
41+
42+
defmodule Phoenix.LiveViewTest.E2E.Issue3656Live.Sticky do
43+
use Phoenix.LiveView
44+
45+
def mount(:not_mounted_at_router, _session, socket) do
46+
{:ok, socket, layout: false}
47+
end
48+
49+
def render(assigns) do
50+
~H"""
51+
<nav>
52+
<.link navigate="/issues/3656?navigated=true">Link 1</.link>
53+
</nav>
54+
"""
55+
end
56+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue3658Live do
2+
# https://github.com/phoenixframework/phoenix_live_view/issues/3658
3+
4+
use Phoenix.LiveView
5+
6+
alias Phoenix.LiveView.JS
7+
8+
def mount(_params, _session, socket) do
9+
{:ok, socket}
10+
end
11+
12+
def render(assigns) do
13+
~H"""
14+
<.link navigate="/issues/3658?navigated=true">Link 1</.link>
15+
16+
{live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3658Live.Sticky,
17+
id: "sticky",
18+
sticky: true
19+
)}
20+
"""
21+
end
22+
end
23+
24+
defmodule Phoenix.LiveViewTest.E2E.Issue3658Live.Sticky do
25+
use Phoenix.LiveView
26+
27+
def mount(:not_mounted_at_router, _session, socket) do
28+
{:ok, socket, layout: false}
29+
end
30+
31+
def render(assigns) do
32+
~H"""
33+
<div>
34+
<div id="foo" phx-remove={Phoenix.LiveView.JS.dispatch("my-event")}>Hi</div>
35+
</div>
36+
"""
37+
end
38+
end

test/e2e/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
160160
live "/3496/b", Issue3496.BLive
161161
live "/3529", Issue3529Live
162162
live "/3651", Issue3651Live
163+
live "/3656", Issue3656Live
164+
live "/3658", Issue3658Live
163165
end
164166
end
165167

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const {test, expect} = require("../../test-fixtures")
2+
const {syncLV, attributeMutations} = require("../../utils")
3+
4+
// https://github.com/phoenixframework/phoenix_live_view/issues/3656
5+
test("phx-click-loading is removed from links in sticky LiveViews", async ({page}) => {
6+
await page.goto("/issues/3656")
7+
await syncLV(page)
8+
9+
let changes = attributeMutations(page, "nav a")
10+
11+
const link = page.getByRole("link", {name: "Link 1"})
12+
await link.click()
13+
14+
await syncLV(page)
15+
await expect(link).not.toHaveClass("phx-click-loading")
16+
17+
expect(await changes()).toEqual(expect.arrayContaining([
18+
{attr: "class", oldValue: null, newValue: "phx-click-loading"},
19+
{attr: "class", oldValue: "phx-click-loading", newValue: ""},
20+
]))
21+
})

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

Lines changed: 15 additions & 0 deletions
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/3658
5+
test("phx-remove elements inside sticky LiveViews are not removed when navigating", async ({page}) => {
6+
await page.goto("/issues/3658")
7+
await syncLV(page)
8+
9+
await expect(page.locator("#foo")).toBeVisible()
10+
await page.getByRole("link", {name: "Link 1"}).click()
11+
12+
await syncLV(page)
13+
// the bug would remove the element
14+
await expect(page.locator("#foo")).toBeVisible()
15+
})

0 commit comments

Comments
 (0)