New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add navigation guards to liveSocket #3051
base: main
Are you sure you want to change the base?
Conversation
8e7d50b
to
eb6732a
Compare
We typically dispatch browsers events for this sort of callbacks. Would it make more sense to use events here too? Or is there a difference between them? |
Events could probably work for the scroll restoration use case, but for preventing navigation this needs to happen synchronously. I guess we could move the actual navigation itself to an event listener and then let users prevent it by calling stopPropagation? |
Oh, good call on being synchronous. Yeah, for me that justifies the difference. :) TY! |
This interface makes sense and it also gives folks a place to perform view transitions. Perhaps we also want the callback to support an async interface as well? |
View transitions are interesting indeed. I'll try to adapt the PR to also allow returning a promise from the callbacks later today. How would the usage with view transitions look like? let liveSocket = new window.LiveView.LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
navigation: {
beforeEach(_to) {
return new Promise((resolve) => document.startViewTransition(() => resolve()));
}
}
}) What I'm still thinking about is the parameters. Probably we want to pass both the old and new route in the callbacks. Do you have any opinion which format would be the best? (Currently I simply use the href string) |
ea429a2
to
c034c38
Compare
I changed to callbacks to Next step: add some tests. |
66622d4
to
39cdbbb
Compare
Still missing: documentation |
You can have a look at view transitions in action: just run |
9bdfcbd
to
2dcfee7
Compare
Docs done, ready for review and final touches. |
490ed31
to
8e89aca
Compare
I believe this patch/approach is in general a good idea, but I don't think the provided example fixes #2107 fully. Consider these steps to reproduce:
This issue goes away by storing the scroll state (or any other state for that matter) in the browser's history state, the same way LiveView already does for the body's scroll position: let state = history.state || {}
state.customElementScrollState = document.getElementById("customElement").scrollTop
history.replaceState(state, "", window.location.href)
// or reuse LV's Browser.updateCurrentState from LV code if that is reachable Obviously, both app code and LiveView code now contest for the same namespace. This is an easy fix, by designating a well known key for either LV, or the user to put their own state, or potentially provide an API. |
That’s a very good point. I’ll try to adapt the API to incorporate the history state. |
livesocket instance: `navigation`. Navigation is an object that can define two functions: `beforeEach` and `afterEach`. The `beforeEach` function is called before each navigation event and can for example be used to store custom scroll positions. If the `beforeEach` function returns `false`, the navigation will be aborted. This can be used to implement a warning when the user tries to navigate while a form is still being edited. The `afterEach` function is called after a navigation event has been completed. It can be used to restore the scroll position that was stored in the `beforeEach` function.
8e89aca
to
03fb8bf
Compare
I tested it in my real world project and the scroll restoration example now works with a 1:1 copy and paste (and setting the data attribute). |
After some further testing, I have found that the
This is reproducible in Firefox and Chrome for me. In general, browsers do retain history state even on forward navigation, so this is not a browser limitation. I don't have a hunch why this happens with the PR (at least not yet). |
@breunigs this is sadly a limitation of the history API. When you navigate to a new page, we save the userData in the "old" history state. The history is like a stack. So starting on page 1 and then navigating to page 2, the history looks like this (new entries on top):
Note that on the new page, the userData is never set. If you now navigate back in the history, the userData is restored. The problem is that you cannot save new data when navigating back, because the browser only tells you that you navigated back after the navigation is already done. It is worth discussing if there should be an API to not need to manually change the history state. Something like
Maybe there should also be another parameter (?) in |
I'll need to think about this a little more. |
Just for my understanding: so with your PR and me navigating back, the order of events is like this:
And obviously after step 3 we can't access the history entry we need to access? If so, then I presume something like https://caniuse.com/mdn-api_navigation_navigate_event would do the trick, but that API isn't even usable in all modern browsers, let alone the ancient ones LV still supports. |
Yes, exactly. |
@SteffenDE I don't want to derail you but have you looked into using the History API for this? I implemented this solution in EmberJS years ago. When pushing history onto the stack you can include state information, which I used to stuff the x,y scroll positions so when the state was popped off the stack and such state was available the scroll position was restored: https://github.com/DockYard/ember-router-scroll/pull/14/files |
@bcardarella yes, actually LiveView already uses the history state for storing the scroll position. I also tried to expose the history state in the more generic navigation guard API proposed in this PR (03fb8bf). I didn't have the time to properly rethink this, but the current problem is that the API proposed here raises some expectations that it cannot fulfill, namely that we can "prevent" a navigation. Without the navigate event, this is always a hack, because we cannot actually prevent a navigation that is caused by a It may be worth splitting the features into:
|
Very interesting ideas here. I'd be particularly interested in the "do you really want to leave?" functionality for user-edited forms. Didn't find any issue tracking that particular problem to subscribe to. Is there a known community-powered solution to that that someone could share a link? 💜 |
I have several multi page forms where one can navigate between steps, I explored a way to prevent data loss when clicking the steps but figured out it was impossible to prevent navigation since there isn't a after navigation hook! This would be a godsend for this layout pattern for sure. 👍 |
This PR introduces a new option that can be specified when creating a livesocket instance:
navigation
. It is inspired by navigation guards in SPA routers like Vue Router.navigation
is an object that can define two functions:beforeEach
andafterEach
. ThebeforeEach
function is called before each navigation event and can for example be used to store custom scroll positions. If thebeforeEach
function returnsfalse
, the navigation will be aborted. This can be used to implement a warning when the user tries to navigate while a form is still being edited. TheafterEach
function is called after a navigation event has been completed. It can be used to restore the scroll position that was stored in thebeforeEach
function.Example to restore a custom scroll position:
Example to prevent navigation when a form submit is pending. This example assumes that the form has a custom attribute, that could be set in a
phx-change
handler:Fixes #2107.
This PR should not be accepted in the current state, but instead just show the idea and maybe discuss how such an API could be optimized, if this is something that we want.