Skip to content

Commit

Permalink
allow putting state into the history
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenDE committed Feb 5, 2024
1 parent 68b88e5 commit 03fb8bf
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 23 deletions.
18 changes: 10 additions & 8 deletions assets/js/phoenix_live_view/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -705,21 +705,21 @@ export default class LiveSocket {
// early return if the navigation does not actually navigate anywhere (hashchange)
if(!this.registerNewLocation(window.location)){ return }
this.withNavigationGuard(window.location.href, previousLocation.href, () => {
let {type, id, root, scroll} = event.state || {}
let {type, id, root, scroll, userData} = event.state || {}
let href = window.location.href

DOM.dispatchEvent(window, "phx:navigate", {detail: {href, patch: type === "patch", pop: true}})
this.requestDOMUpdate(() => {
if(this.main.isConnected() && (type === "patch" && id === this.main.id)){
this.main.pushLinkPatch(href, null, () => {
this.maybeScroll(scroll)
this.afterNavigation(href, previousLocation.href)
this.afterNavigation(href, previousLocation.href, userData)
})
} else {
this.replaceMain(href, null, () => {
if(root){ this.replaceRootHistory() }
this.maybeScroll(scroll)
this.afterNavigation(href, previousLocation.href)
this.afterNavigation(href, previousLocation.href, userData)
})
}
})
Expand Down Expand Up @@ -785,8 +785,9 @@ export default class LiveSocket {
}

pushHistoryPatch(href, linkState, targetEl){
this.withNavigationGuard(href, this.currentLocation.href, () => {
this.withNavigationGuard(href, this.currentLocation.href, (userData) => {
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href) }
if(userData) Browser.updateCurrentState(state => Object.assign(state, {userData}))

this.withPageLoading({to: href, kind: "patch"}, done => {
this.main.pushLinkPatch(href, targetEl, linkRef => {
Expand All @@ -808,8 +809,9 @@ export default class LiveSocket {
}

historyRedirect(href, linkState, flash){
this.withNavigationGuard(href, this.currentLocation.href, () => {
this.withNavigationGuard(href, this.currentLocation.href, (userData) => {
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href, flash) }
if(userData) Browser.updateCurrentState(state => Object.assign(state, {userData}))

// convert to full href if only path prefix
if(/^\/$|^\/[^\/]+.*$/.test(href)){
Expand Down Expand Up @@ -867,16 +869,16 @@ export default class LiveSocket {
if(result === false){
cancel()
} else {
callback()
callback(result)
}
})
}

// must be called after a navigation was performed, see `withNavigationGuard`.
afterNavigation(to, from){
afterNavigation(to, from, userData){
// wait for the DOM to be patched
window.requestAnimationFrame(() => {
this.navigationCallbacks["afterEach"](to, from)
this.navigationCallbacks["afterEach"](to, from, userData)
})
}

Expand Down
26 changes: 16 additions & 10 deletions guides/client/js-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,14 @@ away from a page. The LiveSocket constructor accepts a navigation option with tw
* `beforeEach(to, from)` is called before a navigation event occurs.
If this callback returns `false`, the navigation event is cancelled.
The callback can return a promise, which will be awaited before
allowing the navigation to proceed.
* `afterEach(to, from)` is called after a navigation event is complete.
allowing the navigation to proceed. Any other return value than false
will be stored in the history state and can be accessed in the `afterEach` callback
as third parameter when navigating back in the history.
* `afterEach(to, from, userData)` is called after a navigation event is complete.
The return value of this callback is ignored. Its primary use is
for restoring state that was stored in `beforeEach`,
e.g., restoring the scroll position of custom containers.
e.g., restoring the scroll position of custom containers. The userData parameter
is only available when navigating back in the history. Otherwise it will be undefined.

For example, the following option could be used to prevent navigating away from a page
when there is a form with the `data-submit-pending` attribute:
Expand Down Expand Up @@ -365,24 +368,27 @@ If you have a custom scrollable container, you can use navigation guards like th
```javascript
// app.js

let scrollPositions = {}

let liveSocket = new LiveSocket("/live", Socket, {
// other options left out
// ...
navigation: {
beforeEach() {
let scrollPositions = {}
Array.from(document.querySelectorAll("[data-restore-scroll]")).forEach(el => {
scrollPositions[el.id] = el.scrollTop
})
return { scrollPositions }
},
afterEach() {
afterEach(_to, _from, userData) {
// restore scroll positions
Array.from(document.querySelectorAll("[data-restore-scroll]")).forEach(el => {
if(scrollPositions[el.id]) {
el.scrollTop = scrollPositions[el.id]
if(userData && userData.scrollPositions){
for (let [id, scrollPosition] of Object.entries(userData.scrollPositions)){
let el = document.getElementById(id)
if(el) {
el.scrollTop = scrollPosition
}
}
})
}
}
}
})
Expand Down
19 changes: 14 additions & 5 deletions test/support/e2e/navigation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.Layout do
</script>
<script>
window.navigationEvents = []
let customScrollPosition = null;
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {
Expand All @@ -21,6 +20,8 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.Layout do
async beforeEach(to, from) {
console.log(to, from)
window.navigationEvents.push({ type: "before", to, from });
let customScrollPosition
let preventNavigation = false
// remember custom scroll position
if (document.querySelector("#my-scroll-container")) {
Expand All @@ -29,19 +30,27 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.Layout do
// prevent navigating when form submit is pending
if (document.querySelector("form[data-submit-pending]")) {
return confirm("Do you really want to leave the page?")
preventNavigation = !confirm("Do you really want to leave the page?")
}
if(document.startViewTransition) {
document.startViewTransition();
}
if(preventNavigation) {
return false
} else {
// the return value will be passed to the `afterEach` as third parameter,
// when navigating back to the previous page
return { customScrollPosition }
}
},
async afterEach(to, from) {
async afterEach(to, from, historyState) {
window.navigationEvents.push({ type: "after", to, from });
// restore custom scroll position
if (document.querySelector("#my-scroll-container") && customScrollPosition) {
document.querySelector("#my-scroll-container").scrollTop = customScrollPosition
if (document.querySelector("#my-scroll-container") && historyState && historyState.customScrollPosition) {
document.querySelector("#my-scroll-container").scrollTop = historyState.customScrollPosition
}
}
}
Expand Down

0 comments on commit 03fb8bf

Please sign in to comment.