javascriptbrowser-historypopstate

Prevent the URL from changing when popstate occurs


Hi I'm trying to implement a "Are you sure you want to leave this page?" popup when navigating away from a page with a modified form that hasn't been submitted. The website is a single page app built using a custom framework.

Since all anchor links are handled by the framework it can tell that clicking a side nav link will cause a page change and show a Bootstrap confirm dialog. If the user clicks "Ok" the click goes through and an AJAX call is made to pull the new page content. If they click "Cancel" the modal is dismissed and they stay on the current page.

The part I haven't been able to solve is when a user clicks the "Back" button in their web browser. This triggers the "popstate" event. All navigation/history is managed using the history API since it's a single page app.

The issue is that when someone clicks "Back", the URL changes since "popstate" occurred, then I show my "Are you sure you want to leave?" modal. When the event occurs the URL in your address bar now shows the page you would go to from your history, not the page you're currently viewing. If you click "Cancel" in the Bootstrap confirm you are now left with the wrong URL displayed in your address bar.

event.preventDefault, event.stopPropagation, or return false inside popstate don't stop the URL from changing. AFAIK you can't intercept the "Back" button before "popstate". Trying to window.history.replaceState inside "popstate" doesn't seem to work either.

Does anyone have a solution on how to make this work?

  1. Can you stop the URL from changing in the "popstate" event?
  2. Can you rewrite the history somehow in "popstate" to change the URL back to what it was on the current page and retain the previous entry if you do decide you want it to go through after a modal has been accepted?
  3. Some other solution I haven't thought of?

My original thought was to block the URL change in "popstate" then let the framework trigger the link click to load the new content and change the URL if they click "Yes" but I haven't found a way to do that.

Thanks!


Solution

  • I solved this by doing the following;

    1. Each time a history state is created I generate a timestamp as a guid
    2. The current timestamp is stored in a variable on the top level module
    3. When popstate occurs the incoming history state's guid is compared against the top level modules guid
    4. If the new guid is greater we're going forward, if it's less we're going backward
    5. When the user clicks the back/forward button the hash does change to the incomming URL. If the user clicks cancel it runs history.go( direction ) where direction is 1 or -1 depending on the timestamps. This sets the URL back to what it should be and doesn't do anything weird to the history stack. The top level history variable has a flag to know that we're faking a page change so the link load logic in popstate is not executed.

    The only quirk is if the user does click yes to navigate away when that request is sent and returned and a history object is created you do not update the top level module's guid. If you do the guid comparison will always think you're going backward because the new event you just added will always be the highest number. This may not be an issue if you don't make new requests for history URL's but our framework does so it doesn't display stale data like the browser typically does.

    It's not ideal since you do see the URL change (no content changes) but so far it seems to work well enough. Another solution I found was to store your own site's history in local/session storage and compare URLs instead of using guid timestamps to find the popstate direction. This won't work in Safari's private browsing mode since both storage layers are disabled so I opted for guid's.