kit icon indicating copy to clipboard operation
kit copied to clipboard

pushState/replaceState don't trigger beforeNavigate

Open samal-rasmussen opened this issue 1 year ago • 2 comments

Describe the bug

I just started using pushState/replaceState and realized that they don't trigger beforeNavigate, and so I cannot ask the user to confirm before navigating away from a modal form.

I have a rather big webapp that will need to have complex forms in modals. Users definitely want to be asked for confirmation before losing complex form state.

Reproduction

This modal will ask for confirmation when clicking the cancel button, but not on pressing back in the browser: https://stackblitz.com/edit/sveltejs-kit-template-default-fsoasb?file=src%2Froutes%2FModal.svelte

There's a beforeNavigate that should be showing the confirmation, but it isn't called.

Logs

No response

System Info

Forked the newest Sveltekit Stackblitz.

Severity

serious, but I can work around it

That is that for now I can live with only showing a confirmation when pressing the close button in the ui.

Additional Information

No response

samal-rasmussen avatar Feb 01 '24 15:02 samal-rasmussen

pushState and replaceState do not perform an actual navigation (shallow navigation) as it does not change the underlying route. The only things that do change are:

  • The browser URL.
  • The browser history entry (push/replace).
  • $page.state if specified.

In comparison, (real navigations) using goto on the client, redirect in a load function, clicking on a link, will change these:

  • $page.url, $page.route, etc.
  • Navigation hooks e.g., beforeNavigate.
  • The browser URL.
  • The browser history entry (push/replace with goto).

teemingc avatar Jun 07 '24 16:06 teemingc

So there's no way to intercept a shallow navigation back and show a confirmation dialog before doing the back navigation? Shouldn't this be possible?

samal-rasmussen avatar Jun 07 '24 16:06 samal-rasmussen

+1 experiencing this issue aswel only workaround i found was doing something like this

<svelte:window
    on:popstate={(e) => {
        if (form_is_tainted) {
            history.pushState(null, '', window.location.href);
        }
    }} />

which is discouraged becasue it interferes with svelte kit router

AndreasHald avatar Nov 05 '24 11:11 AndreasHald

@eltigerchino can you see any reason why letting beforeNavigate fire would be an issue? I can't really come up with a reason myself. In that case should this not be considered a bug? since the documentation for shallow routing literally suggests using it as a model that can be dismissed with the back button.

If there is a good reason why beforeNavigate would be a problematic solution could something akin to beforeShallowNavigate and similar hooks be a solution?

AndreasHald avatar Nov 05 '24 12:11 AndreasHald

I also think it's a bug that page params doesn't seem to being updated on popstate (spa mode). Seems like there are multiple issues here.

ref: https://github.com/paoloricciuti/sveltekit-search-params/issues/80#issuecomment-2634340592

niemyjski avatar Feb 04 '25 18:02 niemyjski

Currently experiencing the same issue. Came here looking for a workaround. I'm using forms inside modals and wanted to give the mobile users a way to swipe back instead of clicking on close/cancel. The shallow routing works fine, but now I'm missing a way to show unsaved changes alert if the form is dirty/tainted.

ItsPouria avatar Jul 06 '25 11:07 ItsPouria

No workaround, and no word from anyone on the Svelte team on whether they want to support this.

samal-rasmussen avatar Jul 07 '25 08:07 samal-rasmussen

@Rich-Harris any chance we can get a direction on this on what the team would like done here. Hopefully that drives a community pr.

niemyjski avatar Jul 07 '25 18:07 niemyjski

@niemyjski we can at least provide some discussion, so when a maintainer takes a look they can decide on a direction which we could create a PR for.

Possible solutions

  1. Letting beforeNavigate fire normally on shallow route changes I can see why this may not be the desired solution since @eltigerchino is right, that beforeNavigate signifies changes to the URL which a shallow navigation is not.

  2. Merging normal navigation and shallow navigation events into beforeNavigate This could be simply extending the "type" argument of the beforeNavigate function

beforeNavigate(({type}) => {
     // (type: "form" | "leave" | "link" | "goto" | "popstate") & | "shallow"
});
  1. Adding a beforeStateUpdate hook, that fires when you attempt to change pageState but before it's applied
<script lang="ts">
   pushState('some-page', { foo: 'bar' });
   beforeStateChange((changeEvent) => {
      // changeEvent: {
      //   from: App.PageState
      //   to: App.PageState
      //   cancel: () => void
      // }
   })
</script>

I think this solution would give the most flexibility and possibly be applicable to other shallow navigation scenarios. It would probably be my preferred solution, however it does introduce yet another hook that would need to be documented, maintained and explained.

What are your thoughts on these possible solutions?

AndreasHald avatar Jul 11 '25 06:07 AndreasHald

  1. beforeNavigate signifies changes to the URL which a shallow navigation is not.

@AndreasHald could you explain this more? Since I think you can change the URL in the shallow navigation. From the docs: The first argument to pushState is the URL, relative to the current URL. To stay on the current URL, use ''. from this link.

ItsPouria avatar Jul 11 '25 14:07 ItsPouria

Yeah this is very badly formulated by me, I apologise.

My point was that the whole idea of shallow navigation, is that you are on one page, and you "overlay" another page. You are correct, in that technically the URL in the browser changes to the overlayed page. However the +page.svelte in use, is the original page

So you may imagine a scenario where your routes are like this:

/table /table/{details-page-id}

Where /table displays a table and /table/{details-page-id} displays a page for a single row in the table.

Then you may wan't to display the details page in a modal instead of doing full navigation, to preserve the table state beneath it. This would probably serve as a better user experience.

So when you click a row you navigate shallowly, which means the +page of /table is still in effect, you just load code and data for another page and display that on top of it. And you do change the URL to that of /table/{details-page-id} which means that if you copy the url of a table with a details page opened you correctly go to that page.

So you DO change the URL but not the route / page you are on.

AndreasHald avatar Jul 11 '25 18:07 AndreasHald

@AndreasHald Thanks for the very thorough explanation, I think I understand how the shallow routing works now. Hopefully we'll get an answer from the team which could drive a community pull-request. I can see this requested feature being used in bigger applications later on down the road.

ItsPouria avatar Jul 13 '25 12:07 ItsPouria