angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
95.1k stars 24.9k forks source link

Deactivation Guard breaks the routing history #13586

Closed patrickracicot closed 2 years ago

patrickracicot commented 7 years ago

I'm submitting a ... (check one with "x")

[X ] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior Currently, I have a deactivation guard on a route which will return false or true depending on a condition. To get to this guarded route, the user must pass though 3 navigation step. Now, once on the guarded route, when using location.back(), the guard is called. If it returns true, the previous route is loaded. If the guard returns false, the navigation is cancelled. But if we redo a Location.back() after a failed navigation, the previous route that will be loaded will be 2 steps in the history instead of 1 (user perception).

Workflow

Main    -- navigate to     --> Route 1
Route 1 -- navigate to     --> Route 2
Route 2 -- navigate to     --> Route 3
Route 3 -- location.back() --> guard returns true  --> Route 2
Route 2 -- navigate to     --> Route 3
Route 3 -- location.back() --> guard returns false --> Route 3
Route 3 -- location.back() --> guard returns true  --> Route 1  (should be Route 2)

Expected behavior An expected behavior for a user would be that navigating back brings back to the previous routed page. Workflow

Main    -- navigate to     --> Route 1
Route 1 -- navigate to     --> Route 2
Route 2 -- navigate to     --> Route 3
Route 3 -- location.back() --> guard returns true  --> Route 2
Route 2 -- navigate to     --> Route 3
Route 3 -- location.back() --> guard returns false --> Route 3
Route 3 -- location.back() --> guard returns true  --> Route 2  (expected)

Minimal reproduction of the problem with instructions Plnkr

  1. Click button Nav to route1
  2. Click button Nav to route2
  3. Click button Nav to route3
  4. Click button Block Nav Back
  5. Click button Nav back
    • BOGUE: The location.back() routed on Route1 instead of Route2

Personnal investigation After some investigation, I saw that in routerState$.then (router.ts line 752) this logic used when navigationIsSuccessful == false is pretty simply but it is the actual cause of this bug. Basically, when a deactivation guard is hit, the location of the browser is already changed to the previous route. Which means that when the guard returns false, the routerState$ runs his logic and calls resetUrlToCurrentUrlTree(). At this point we can see that we replace the state of the current location. But by doing this, we loose that route in the history which means that in my plunker, if we click the block nav back 3 times and then click the nav back we will actually kill the application.

What is the motivation / use case for changing the behavior? This is for me a pretty big bug since a guard that returns false breaks alters the current routing history. In the case of our application this breaks the workflow and brings wrong business scopes to a user.

Please tell us about your environment:

Windows 10, NPM, Nodejs, Visual Studio 2015 (using nodejs for typescript compilation)

KevinKelchen commented 5 years ago

I would love to see this issue fixed! ❤️ I am experiencing it in 7.2.1.

Timing-wise, the browser's location and history has changed before the Deactivate Guard fires. If a false-like value is returned from canDeactivate() Angular will revert the browser's location but will not modify the history. Since the navigation was canceled, not correcting the history presents a problem.

At this time I'm not coming up with a workaround that solves all of the cases. The best I have so far, based somewhat on this comment, handles the browser back button, Location.back(), and imperative navigation. Browser forward button, and likely Location.forward(), do not work. Also, it results in new state being pushed on the browser history stack, so entries in the history stack that you could forward navigate to will be lost. 🙁

import { Location } from "@angular/common";
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanDeactivate, Router, RouterStateSnapshot } from "@angular/router";

@Injectable({
  providedIn: "root"
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {

  constructor(
    private location: Location,
    private router: Router
  ) { }

  canDeactivate(component: CanComponentDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) {
    return component.canDeactivate ? component.canDeactivate().then(canDeactivate => {
      if (!canDeactivate && this.router.getCurrentNavigation().trigger === "popstate") {
        this.location.go(currentState.url);
      }

      return canDeactivate;
    }) : true;
  }
}

export interface CanComponentDeactivate {
  canDeactivate: () => Promise<boolean>;
}
hirenrojasara commented 5 years ago

working @KevinKelchen

WillEllis commented 5 years ago

This issue is even worse if you use replace state. E.g. route to list/{id}/edit call location.replaceState('list/{modifiedID}/edit') try to route away, return false from canDeactivate location is updated to list/{id}/edit.

@KevinKelchen 's solution still has list/{id}/edit as the currentState.url. None of the snapshots have the replaced state of list/{modifiedID}/edit

Any idea if a fix will be put in for this anytime soon, seems like its been open for a while.....

qmarquez commented 5 years ago

issue still persisting on angular router 7.2.6!!

SlyEzo commented 5 years ago

I also am experiencing this issue in production. Is there any way we can have a ball park range of when this is going to be fixed?

zvonimirprofico commented 5 years ago

Also experiencing this. As @WillEllis pointed out in regards to @KevinKelchen solution and solutions that update history state, changing parameters does break functionality. The workaround by hooking up onbeforeunload when the route is loaded does its job, but CanDeactivates behavior is not as documented. An update/status on this would be appreciated.

Da13Harris commented 5 years ago

The only person who had attempted a PR on this was @DzmitryShylovich, who has been banned from the project due to multiple Code of Conduct violations.

I really hope this is on the Angular core team's radar. We have a production application that uses CanDeactivate to detect unsaved changes. This bug can lead to significant data issues and a handful of unpredictable behaviors throughout the app.

DanielHabenicht commented 5 years ago

Wow, this bug has a long history. There is a PR for it at #18135 but it is not merged because it does not implement a bugfix for forward navigation. @jasonaden maybe you want to reopen the PR as more than one year hasn't brought up a fix for forward navigation? Not fixing a bug because we could potentially fix another one is, in my mind, a bad excuse for not letting the bug fix pass.

dmytroyarmak commented 5 years ago

We have the same issue in our application. We have confirmation dialog on deactivate guard and when a user clicks back button multiple times we have several problems:

kapilSoni101 commented 5 years ago

@icesmith: sir in which section we have put above code i am also facing same issue?

gowthaman-murugan commented 5 years ago

I am facing the same issue in Angular 6.2.8, imperative to popstate navigation can-deactivate, not working

my code

export class CanDeactivateGuard implements CanDeactivate<UserFormComponent> {
  canDeactivate(component: UserFormComponent): boolean {

    if(component.hasUnsavedData()){
        if (confirm(You have unsaved changes! If you leave, your changes will be lost.)) {
            return true;
        } else {
            return false;
        }
    }
    return true;
  }
}

The use case for reproducing: If I click navigation menu show prompt, now I clicked cancel button(stay on the page) and click browser back button "CanDeactivateGuard " not working, if I click browser back button more than one it's worked

mohammadrafigh commented 5 years ago

I don't know why it's not fixed after 3 years. Angular guards are not working properly in a normal case to close a dialog when back button is pressed because the history is changing even if the guard returns false. it's funny that the frequency label is set to low for a core functionality!! and the PRs are closed because they were just resolving the back button issue and not forward!!

coffeemesh commented 4 years ago

I guess I have to tell my client not to use the back button!

kameelyan commented 4 years ago

Sad it's been 3 years, but @KevinKelchen's solution is close enough for me to use it.

zvonimirprofico commented 4 years ago

It's close enough, but it will still break on parameters change. To prevent refreshes and route changes, the approach taken is to prompt the user with a browser message. This is done in the component on which your guard acts upon.

@HostListener('window:beforeunload') preventLeavingPage(): Observable<boolean> | boolean { return yourConditionForLeaving; }

Finding more information on this should not be a problem, a large number of people are settled with this and can give better advice on how to go about it.

alexdabast commented 4 years ago

With the increase in popularity of PWA applications, users of android device use a lot the back button to close dialog or to go back, this really should be fix

naveedahmed1 commented 4 years ago

Opened since 2016, I can confirm that the issue still exists in 9.0.0-next.7, any update on this?

PapyElGringo commented 4 years ago

Building a PWA today and facing this same issue...

anshultechy commented 4 years ago

below code worked for me

import { Injectable } from '@angular/core'; import { CanDeactivate, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { SettingsPageComponent } from '../components/settings-page/settings-page.component';

@Injectable() export class ConfirmDeactivateGuardForSettings implements CanDeactivate {

canDeactivate(target: SettingsPageComponent ,route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { debugger; if (target.hasChanges) { var res = window.confirm('Do you really want to cancel?');

  if (res) {
  }
  return res;
    }
    return true;
}

}

  return res;
    }
    return true;
}
eduardolozano1993 commented 4 years ago

I can´t believe a feature like this is broken. Lets hope 2020 is the year they fix it.

artbat commented 4 years ago

2016? It's impossible to have a tight control of navigation if I can't lock the user to an URL. Specially in mobile apps. This situation (2016?, really?) compromises the decision of using Angular for my latest project.

piotrkabacinski commented 4 years ago

I just faced the same issue (Angular 9.x) and came up with a simple workaround that seems to fix it in non-Chromium browsers (Firefox), I'm not proud of it, not sure it will cover all cases, but for now, looks like working:

// npm i bowser
const browser = Bowser.getParser(window.navigator.userAgent).getBrowser();
const sleep = (ms: number): Promise<undefined> =>
  new Promise(resolve => setTimeout(resolve, ms));

// ...

async canDeactivate(component, currentRoute, currentState, nextState) {
    // Some truthy conditions...

   // To avoid routing race condition pause the method's body execution and push to the end of the stack 
   if (/Firefox/.test(browser.name) === false) {
     await sleep(0);
   }

    this.router.navigate([/* desired path */], {
      queryParams: {
        // ...
      }
    });

  return false;
}
antpyykk commented 4 years ago

This same problem is also affecting also our product. It's a bit odd that this isn't resolved. Seems like a common use case. Maybe now that Ivy is out the team can concentrate on improving the existing features.


Many good solutions presented before. I've tried various ways of implementing a possible way around the issue, but all seem to have some problems / limitations.

I wanted to chime in with my own hacky PoC, that seems to work and gather feedback on potential pitfalls.

Note:

The main idea is to temporarily ignore and prevent the propagation of history change events to the router. We don't want the router to react to our manual popstate events that will restore history state once a guard is cancelled.

To do this, we want to block the LocationStrategy's replaceState and pushState calls. So we will create a new location strategy that will extend the default path strategy (HistoryBlockingLocationStrategy).

Continuing where KevinKelchen left off.

import { Location } from "@angular/common";
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from "@angular/router";

@Injectable({
  providedIn: "root"
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {

  constructor(
    private location: Location,
    private locationStrategy: HistoryBlockingLocationStrategy 
  ) { }

  canDeactivate(component: CanComponentDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) {
    return component.canDeactivate ? component.canDeactivate().then(canDeactivate => {
      if (!canDeactivate && this.router.getCurrentNavigation().trigger === "popstate") {
       // Assuming it's a back navigation (forward not in scope of this PoC)
        this.locationStrategy.stopNextPopstatePropagation(); // Stops the next popstate event from being propagated to router
        this.location.forward(); // Now navigate forward in history state
      }

      return canDeactivate;
    }) : true;
  }
}

export interface CanComponentDeactivate {
  canDeactivate: () => Promise<boolean>;
}

New blocking location strategy that should be provided for the app

export class HistoryBlockingLocationStrategy extends PathLocationStrategy {
    private historyBlocked: boolean = false;

    /* Stops next popstate call from being passed on to the router */
    public stopNextPopstatePropagation(): void {
      this.historyBlocked = true;
    }

    public unblockHistory(): void {
      this.historyBlocked = false;
    }

   /* Wrapping all incoming popstate listener events. This is how router gets it's updates */
    onPopState(fn: LocationChangeListener): void {
        const wrappedFn: LocationChangeListener = (event: LocationChangeEvent) => {
            if (this.historyBlocked) { // Popstate call was received, but we don't want the router to react to it
                this.unblockHistory(); // The propagation was stopped, now return back to normal

                return () => {} // noop event listener
            }

            return fn(event);
        }
        super.onPopState(wrappedFn);
    }

    /* Replace state is being called after guards reject routing. Results in duplicate url calls if back button is pressed */
    replaceState(state: any, title: string, url: string, queryParams: string): void {
        if (this.historyBlocked) { 
          return;
        }

        return super.replaceState(state, title, url, queryParams);
    }
}

Again, this is just a PoC that could be improved in many ways. My main concern is that blocking popstate events might cause some issues with the router / location syncing. However my quick testing didn't seem to have problems with this.

Is there some downside in blocking popstate calls?

russalert commented 4 years ago

I found one WA: I have implemented Abstract Component with HostListener:

can-deactivate.component.ts

import {HostListener} from '@angular/core';

export abstract class ComponentCanDeactivate {

  abstract canDeactivate(): boolean;

  @HostListener('window:beforeunload', ['$event'])
  unloadNotification($event: any) {
    if (!this.canDeactivate()) {
      $event.returnValue = true;
    }
  }
}

can-deactivate.guard.ts

import {CanDeactivate} from '@angular/router';

import {Injectable} from '@angular/core';
import {ComponentCanDeactivate} from './can-deactivate.component';

@Injectable({
  providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<ComponentCanDeactivate> {
  canDeactivate(component: ComponentCanDeactivate): boolean {

    if(!component.canDeactivate()){
      if (confirm("You have unsaved changes! If you leave, your changes will be lost.")) {
        return true;
      } else {
        return false;
      }
    }
    return true;
  }
}

myform.component.ts

import {ComponentCanDeactivate} from '../shared/guard/can-deactivate.component';
export class MyComponent extends ComponentCanDeactivate {
....
  canDeactivate():boolean {
    return !this.form.dirty;
  }
.....
}
artbat commented 4 years ago

Again, this is just a PoC that could be improved in many ways. My main concern is that blocking popstate events might cause some issues with the router / location syncing. However my quick testing didn't seem to have problems with this.

Is there some downside in blocking popstate calls?

This looks promising for my case, but how do you configure Angular to use the alternate Location Strategy?

amdor commented 4 years ago

{ provide: LocationStrategy, useClass: HistoryBlockingLocationStrategy }, in NgModule

atscott commented 4 years ago

Confirmed in v9: https://stackblitz.com/edit/angular-ivy-tefqm1?file=src%2Findex.html

Edit: example also extended to show that a similar issue exists with guards with Location#forward so we can close #15664 as a duplicate.

atscott commented 4 years ago

Update after some investigation:

While those who have attempted to address this issue in the past are no longer on the team, I can at least make a reasonable guess as to why this hasn't been fixed. I don't really have an update for what can/will be done to address these issues. I think it's worth documenting the difficulty with this, though; I haven't really seen anything from the previous attempts at it so I had to do my own investigation.

Many people in this thread have responded with reasonable solutions to the particular issue with Location#back()/the browser back button. It's worth noting that there is nothing that can address both back and forward actions.

Because of the limitations with the browser history and events, any potential fix for this would be a bit messy and still not work for all scenarios. So there's maybe no "right" way to fix this issue so it's difficult to determine what direction to go in making a change.

atscott commented 4 years ago

Another update on this: I'm trying to address at least part of this issue with imperative navigations (ones which are triggered by router.navigate) that get cancelled/blocked by guards and are followed by a popstate/hashchange (browser back/forward or manual url change) in #37408. The change initially appears to resolve one of the tests that was added in the previous attempts to correct this issue. I've actually encountered this in the past week when investigating another report: https://github.com/angular/angular/issues/16710#issuecomment-634869739. If the presubmits look good as well, we can hopefully get this submitted as a non-breaking change bug fix that doesn't require a new router config option.

In order to address the issue with history and browser back/forward navigation, I think there could be an opportunity to add a beforeUnload listener that executes synchronous canDeactivate guards to potentially prevent any browser navigation. This feels like a decent option that would have somewhat well-defined behavior and expectations that could be documented. This change would need much more thought and design work, but I'm documenting the option here for future reference. This wouldn't work for SPAs in all cases except for when the back/forward is navigating to an external page.

redyaris commented 4 years ago

The main problem, in my view, is that the router by itself has no way of knowing whether a popstate event is a forward or back click (imperative navigation is a separate issue, but more easily dealt with--see below). If the router knew whether forward or back was clicked, it could simply call location.forward() when a back-click navigation is cancelled by a deactivation route guard, and location.back()when a forward-click navigation is cancelled.

However, because the router doesn't have that information, when a deactivation gaurd cancels a popstate navigation, the router restores the prior URL by simply calling replaceState() to overwrite the current state (which was set to the now-cancelled URL as soon as the popstate event fired) with the prior URL. When that happens, history gets corrupted because there are now 2 entries in the history stack containing the prior URL: the original, which was the current state prior to the cancelled navigation, and the now current state, which was overwritten with the prior URL when the navigation was cancelled.

The solution I've implemented to work around this is to utilize a global service RouterHistoryTrackerService, injected in the root component, that tracks all navigation changes by subscribing to router events (NavigationStart, NavigationEnd, NavigationCancel).

It tracks the router event id of the last navigation and the direction of the last navigation relative to the current state. Then, on a popstate event, it compares the restoredState NavigationId of that popstate navigation to the id of the last navigation. If they match, then the user is going in the stored direction. If not, the user is going the other way.

Once direction has been determined, if the navigation is canceled by a routeGuard, all that’s left to do is to reverse the replaceState() performed by the router (by calling replaceState() again, this time with the cancelled url so it goes back on the stack in its place) and then call location.back() (if the cancelled popstate was forward) or location.forward() (if the cancelled popstate was back).

Imperative navigation are also tracked, but those are handled differently by the router when cancelled. State never changes when an imperative navigation is canceled, so does not need to be restored. However, the cancelled URL still gets stored as the lastNavigation within the router (this is the rawUrl value within the transitions object in the router). To prevent a subsequent popstate to the cancelled URL from being ignored, I use a non-state-changing navigation (skipLocation) to the current URL. (Note: I only deal with imperative and popstate navigation, not hashchange).

While RouterHistoryTrackerService is always running to track history, its cancellation handler only gets invoked when guardInvoked is set to true, which is done in the routeGuard by calling setGuardInvoked(true) for guarded routes. I use a global generic routeGuard service (CanDeactivateComponentService), which makes this call and then defers to the canDeactivate()method of each guarded component to set conditions for deactivation.

If navigation tracking logic of this type could be built into the router, it could avoid using the replaceState() calls when a routeGuard blocks deactivation and instead just call location.forwad() or location.back() as appropriate.

router-history-tracker-service.ts

/*
 * RouterHistoryTrackerService: maintains record of current and prior navigation.
 * When injected into a CanDeactivate guard and invoked in that guard,
 * prevents corruption of history stack for both imperative and popstate navigation
 * 
 * //todo: consider migrating to localstorage for tracking variables for more
 * robustness on refresh.
 * 
 * Copyright (c) 2020 Adam Cohen redyarisor [at] gmail.com
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this 
 * software and associated documentation files (the "Software"), to deal in the Software 
 * without restriction, including without limitation the rights to use, copy, modify, merge, 
 * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
 * to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all 
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 * OTHER DEALINGS IN THE SOFTWARE.
 * 
 */

import { Injectable, OnDestroy } from '@angular/core';
import { Router, NavigationEnd, NavigationStart, NavigationCancel, PRIMARY_OUTLET, RouterEvent } from '@angular/router';
import { Subscription } from 'rxjs';
import { Location } from '@angular/common';
import { filter } from 'rxjs/operators';

//singleton service
@Injectable({
  providedIn: 'root'
})

export class RouterHistoryTrackerService implements OnDestroy {

  // subscription to router events to track changes
  private routerSubscription : Subscription;

  // url of current page (last successful navigation)
  private urlCurrent: string;
  // url of current page, parsed into segments (e.g. ["home","samplepage","1"])
  private urlCurrentSegmented : Array<string>;
  // id of current url
  private navIdCurrent : number; 
  // state of current page
  private stateCurrent : any;  

  //event id of most recent PRIOR navigation. Set by NavigationEnd handler.
  //If current page was reached imperatively or by clicking forward, id of page reached by going back
  //If current page was reached by going back, id of page reached by going forward  
  private navIdHistorical : number;

  // direction to get to navIdHistorical (forward or back)
  // used in conjunction with navIdHistorical to determine
  // whether a popstate event is forward or back
  // if a popstate is navigating to an id that matches navIdHistorical
  // then we know we're going in navDirectionHistorical, otherwise, we're going the other way
  // set to "back" after an imperative navigation because only historical option is "back" after an imperative
  // navigation event, which becomes the new top-of-the stack
  private navDirectionHistorical : string; 

  // holds restoredState nav id of current navigation. Populated whenever a popstate occurs
  private restoredStateNavId : number;

  // tracks whether currently active navigation was imperative or the result of a popstate
  private poppedState : boolean = false;

  // direction of current navigation
  // set each time a NavigationStart event occurs if triggered by popstate 
  // determined by comparing destination of NavigationStart event to
  // navDirectionHistorical and navIdHistorical
  // initialized by first popstate NavigationStart
  private direction : string; 

  //information needed to restore an item in the history stack
  //captured with each NavigationStart event
  private restoreState : any;
  private restoreUrl : string;

  // was cancellation invoked by a routeGuard?  If so, apply special procedures (below)
  // set and managed by each routeGuard; allows tracking to continue for any navigation
  // to preserve history, but only triggers restorative behavior when a guard is invoked 
  // and navigation cancelled.
  private guardInvoked : boolean;

  constructor(private router : Router, private location : Location) {
    this.urlCurrent = this.router.url;

    // set up subscription to router Start, End, and Cancel events for tracking and handling
    this.routerSubscription = router.events.pipe(
      filter((event : RouterEvent) => event instanceof NavigationStart || event instanceof NavigationEnd || event instanceof NavigationCancel))
      .subscribe(event =>      
      {
      if (event instanceof NavigationStart){
        this.navigationStartHandler(event);
      }      
      else if (event instanceof NavigationEnd) {
        this.navigationEndHandler(event)
      }  
      else if (event instanceof NavigationCancel){
        this.navigationCancelHandler(event);
      }
    });
  }

  /**
  * Handler for navigationStartEvents
  * Determines direction of travel if popstate-triggered
  * Captures destination state info for use in event of cancellation
  * @param event a NavigationStart event
  */
  private navigationStartHandler(event: NavigationStart){
    // popstate = back or forward clicked
    // if popstate, need to determine if it was back for forward pressed 
    if (event.navigationTrigger == 'popstate'){
      this.poppedState = true;

      // restored state id is part of the NavigationStart event on a popstate event; capture it 
      this.restoredStateNavId = event.restoredState ? event.restoredState.navigationId : null;

      // if restored state matches stored, then we're going in stored direction
      if (this.restoredStateNavId == this.navIdHistorical)
      { 
        this.direction = this.navDirectionHistorical;
      }
      // otherwise, we're going in the opposite direction
      else{
        this.direction = this.navDirectionHistorical == 'back' ? 'forward' : 'back';
      }
    }
    //no popstate = imperative;
    else{
      this.direction = "imperative";
    }
    // on any navigation start, capture the details of the website we're starting to go to
    // in case it needs to be replaced in the stack (in the event of a popstate cancellation)
    this.setRestoreParams(this.location.path(), history.state);

  }

  /**
   * Handler for nagiationEnd Events
   * after a completed navigation:
   * 1. sets navDirectionHistorical which is the direction to get to the last-visited page.
   * 2. sets navIdHistorical which is the navigationId of the last-visited page
   * 3. captures current page info
   * 
   * @param event a NavigationEnd Event
   */
  private navigationEndHandler(event: NavigationEnd){
      // if navigating imperatively, the only available direction is back because top 
      // of history stack is being replaced and the prior page is now accessible by going back
      if (!this.poppedState){ // imperative navigation
        this.navDirectionHistorical = "back";
      } 
      // if navigation was triggered by popstate
      // and we just went forward, prior page is now accessible by clicking back
      // reset popped state for next navigation
      else if (this.direction == "forward"){
        this.navDirectionHistorical = "back";
        this.poppedState = false;          
      } 
      // if navigation was triggered by popstate and we just went back,
      // prior page is now accessible by clicking forward       
      // reset popped state for next navigation        
      else {
        this.navDirectionHistorical = "forward";
        this.poppedState = false;
      }
      // capture PRIOR navigation event Id 
      // (which was stored in current after last navigation end)
      // and store it for comparison on next navigation 
      this.navIdHistorical = this.navIdCurrent;      

      // update current id, url, and state
      this.urlCurrent = event.url;   
      this.navIdCurrent = event.id;        
      this.stateCurrent = history.state;   

      // parse current URL into segments to allow use of router.navigate to clean up after an imperative navigation below
      this.urlCurrentSegmented = new Array<string>()        
      let k = this.router.parseUrl(event.url).root.children[PRIMARY_OUTLET]
      if (k){
        k.segments.forEach(element => this.urlCurrentSegmented.push(element.path));
      }

      // after successful navigation, clear restoredId and direction
      this.clearDirectionAndRestoredId();    
  }

  /**
   * handle cancellations for imperative and popstate navigation
   * @param event 
   */
  private navigationCancelHandler(event: NavigationCancel){
      // early exit if cancellation was not guard-invoked (set by authGuard)
      if (!this.guardInvoked){
        return; 
      }
      /*
      Special case for an imperative navigation
      A cancelled imperative naviagtion still gets stored as the lastNavigation 
      within the router (this is the rawUrl value within the transitions object in the router).
      Because cancelled imperative navigations do not change the router 
      state (url never changes; replaceState never called), we do not need to adjust state.
      However having the lastNavigation set to the cancelled URL is a problem, 
      if the lastNavigation is the same as what we are going back or forward to 
      to (i.e. page1 -[imperative]-> page2 -[imperative]-> page1[cancel] -[back]-> page1
      Under those circumstances, the router sees the lastNavigation and the actual previous 
      navigation url as the same and skips the navigation by default.
      To get around this, we fire a non-state-changing navigation to the current URL, 
      which doesn't touch state, but updates the lastNavigation stored by the router to
      reflect the current URL.  Enclosed in a timeout to prevent a navigation ID mismatch error
      when the router is changed multiple times in the same cycle
      */
      if (!this.poppedState){
        setTimeout(() => {
          this.router.navigate(this.urlCurrentSegmented, {skipLocationChange: true});
        });
      }

      /* Popstate cancellation:
      On a popstate event, state (URL) changes as soon as navigation begins.
      On a popped state cancellation, the Angular will restore the state by calling replaceState
      on the new (cancelled) URL, replacing it with the URL that we started at.  
      This action breaks history because the URL we cancelled gets removed from the stack 
      (clobbered by the URL we started at during the replaceState) and there are now 2 entries 
      on the stack for the URL we started at (one as a result of the replaceState and the original)
      To fix the issue, we reverse the router's replaceState by calling replaceState 
      with the URL we cancelled so that it is put back in the correct stack position, 
      and then we roll back the popstate by navigating back (if forward was clicked)
      or forward (if back was clicked)
      */

      // must have a restoreUrl (set in navigationStartHandler) to proceed
      if (this.poppedState && this.restoreUrl != undefined){
        // reverse router's replaceState call by putting the cancelled destination URL back in the stack
        this.location.replaceState(this.restoreUrl, undefined, this.restoreState);

        if (this.direction == "forward"){
          this.location.back();
        }
        else {
          this.location.forward();
        }
        // clear restore params for next navigation
        this.resetRestoreParams();  
        this.setGuardInvoked(false);
      }
      //reset popstate tracker for next navigation
      this.poppedState = false;

    }

  // getters/helper methods
  public getNavIdStored(){
    return this.navIdHistorical;
  }

  public getNavDirectionStored(){
    return this.navDirectionHistorical;
  }

  public getNavIdIdRestored(){
    return this.restoredStateNavId ? this.restoredStateNavId : null;
  }

  public getDirection(){
    return this.direction;
  }

  // called from routeGuard to invoke cancellation handler
  public setGuardInvoked(guardStatus : boolean){
    this.guardInvoked = guardStatus;
  }

  // clear direction and restoredStateNavId for next run
  private clearDirectionAndRestoredId(){
    this.direction, this.restoredStateNavId = undefined;
  }

  // sets restore parameters for restoring stack after nav cancellation
  private setRestoreParams( restoreUrl : string, restoreState : any){    
      this.restoreUrl = restoreUrl;
      this.restoreState = restoreState;        
  }

  // resets restore parameters
  private resetRestoreParams(){
        this.restoreUrl, this.restoreState = undefined;
  }

  public ngOnDestroy(){
    this.routerSubscription.unsubscribe();
  }

}

can-deactivate-component-service.ts

/** 
 * 
 * CanDeactivateComponentService: a basic deactivation routeGuard.  Used in conjunction
 * with RouterHistoryTracker.  When added as a routeGuard to a component, it enables
 * the history protection features of RouterHistoryTracker so that if deactivation is disallowed
 * the historical stack is preserved.
 * 
 * Copyright (c) 2020 Adam Cohen redyarisor [at] gmail.com
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this 
 * software and associated documentation files (the "Software"), to deal in the Software 
 * without restriction, including without limitation the rights to use, copy, modify, merge, 
 * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
 * to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all 
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 * OTHER DEALINGS IN THE SOFTWARE.
 * 
 */

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import { Observable } from 'rxjs';
import { RouterHistoryTrackerService } from './router-history-tracker.service';

export interface CanDeactivateComponent {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable({
  providedIn: 'root'
})
export class CanDeactivateComponentService implements CanDeactivate<CanDeactivateComponent>{

  constructor(private routerTracker : RouterHistoryTrackerService){
  }

  // if canDeactivate method (which is a property of the CanDeactivateComponent interface) is defined for component,
  // return result of component's canDeactivate method, otherwise allow deactivation.
  canDeactivate(component : CanDeactivateComponent, currentRoute : ActivatedRouteSnapshot, currentState : RouterStateSnapshot){
    if (component.canDeactivate == undefined){    
      return; 
    }

    else {     
      //invoke history protection features of Router History Tracker Service to allow restoration
      //if navigation is cancelled.
      this.routerTracker.setGuardInvoked(true);      
      return component.canDeactivate();
    }
  }  
}
petebacondarwin commented 4 years ago

@redyaris - thanks for this excellent comment. I think that this is the direction [sic] that @atscott is thinking of going to solve this problem. Watch this space...

redyaris commented 4 years ago

@petebacondarwin - You're welcome. Nice to hear this may be the direction things are headed.

One other benefit of this approach that I forgot to mention earlier: the router history tracker service can be injected into components to make them direction-aware, which can be used within their canDeactivate() methods to implement different guard behavior for different directions/navigation types.

antpyykk commented 4 years ago

Glad that this is finally getting traction. Great solution from @redyaris! We tackled the problem of back / forward detection using a generated timestamp on history.state.

For most cases this should resolve the problem.

One edge case that we found hard to tackle is right clicking the forward / back button and choosing an entry that is more than one step away from the currently active state. Simply using back/forward is not going to cut it. We did some additional steps for restoring these types of navigations, but they wound up breaking the history stack.

Perhaps someone else can come up with an elegant solution :)

Memeplexx commented 3 years ago

If anyone is interested, I have posted my solution on stackoverflow which includes a more detailed explanation as well as a Stackblitz demo.

tce-kailash commented 3 years ago

Hi @StephenPaul,

I was playing around with your poc and it seems i am doing the same in my poc

history.pushState(null, '', '');

But this approach clears up all the forward history and only back history is maintained.

Memeplexx commented 3 years ago

Thanks for the heads up @tkailash. I will make note that this as a compromise of the solution. Sadly there seem to be only hacks around the problem at this state.

tce-kailash commented 3 years ago

Sure @StephenPaul i think we all are trying the same to bring up a better solution or at-least AWA, i think we have to wait until this is fixed.

leekFreak commented 3 years ago

@redyaris, I tried your solution, and I'm hitting an error in navigationCancelHandler, because this.urlCurrentSegmented is undefined, if my cancel is the first navigation I do after my first routerLink. After the error, the cancel button has no effect, no popup, and no navigation. If I leave the page by clicking something else then the cancel button and I come back, then it's fine. If I start with something else but the cancel button, it's also fine.

I tried adding // parse current URL into segments to allow use of router.navigate to clean up after an imperative navigation below this.urlCurrentSegmented = new Array(); const k = this.router.parseUrl(event.url).root.children[PRIMARY_OUTLET]; if (k) { k.segments.forEach(element => this.urlCurrentSegmented.push(element.path)); }

in navigationCancelHandler, since even.url is defined, but then the popup enters an infinite loop.

I tried initializing urlCurrentSegmented private urlCurrentSegmented: Array = [];

The error is not reported, but it still breaks the cancel button.

Any idea?

lwk618 commented 3 years ago

@redyaris, I tried your solution, and I'm hitting an error in navigationCancelHandler, because this.urlCurrentSegmented is undefined, if my cancel is the first navigation I do after my first routerLink. After the error, the cancel button has no effect, no popup, and no navigation. If I leave the page by clicking something else then the cancel button and I come back, then it's fine. If I start with something else but the cancel button, it's also fine.

I tried adding // parse current URL into segments to allow use of router.navigate to clean up after an imperative navigation below this.urlCurrentSegmented = new Array(); const k = this.router.parseUrl(event.url).root.children[PRIMARY_OUTLET]; if (k) { k.segments.forEach(element => this.urlCurrentSegmented.push(element.path)); }

in navigationCancelHandler, since even.url is defined, but then the popup enters an infinite loop.

I tried initializing urlCurrentSegmented private urlCurrentSegmented: Array = [];

The error is not reported, but it still breaks the cancel button.

Any idea?

This is my solution based on @redyaris

 * RouterHistoryTrackerService: maintains record of current and prior navigation.
 * When injected into a CanDeactivate guard and invoked in that guard,
 * prevents corruption of history stack for both imperative and popstate navigation
 *
 * //todo: consider migrating to localstorage for tracking variables for more
 * robustness on refresh.
 *
 * Copyright (c) 2020 Adam Cohen redyarisor [at] gmail.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this
 * software and associated documentation files (the "Software"), to deal in the Software
 * without restriction, including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
 * to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 */

import { Injectable, OnDestroy } from '@angular/core';
import { Router, NavigationEnd, NavigationStart, NavigationCancel, PRIMARY_OUTLET, RouterEvent } from '@angular/router';
import { Subscription } from 'rxjs';
import { Location } from '@angular/common';
import { filter } from 'rxjs/operators';

// singleton service
@Injectable({
  providedIn: 'root'
})

export class RouterHistoryTrackerService implements OnDestroy {

  // subscription to router events to track changes
  private routerSubscription: Subscription;

  // url of current page (last successful navigation)
  private urlCurrent: string;
  // url of current page, parsed into segments (e.g. ["home","samplepage","1"])
  private urlCurrentSegmented: Array<string>;
  // id of current url
  private navIdCurrent: number;
  // state of current page
  private stateCurrent: any;

  // event id of most recent PRIOR navigation. Set by NavigationEnd handler.
  // If current page was reached imperatively or by clicking forward, id of page reached by going back
  // If current page was reached by going back, id of page reached by going forward
  private navIdHistorical: number;

  // direction to get to navIdHistorical (forward or back)
  // used in conjunction with navIdHistorical to determine
  // whether a popstate event is forward or back
  // if a popstate is navigating to an id that matches navIdHistorical
  // then we know we're going in navDirectionHistorical, otherwise, we're going the other way
  // set to "back" after an imperative navigation because only historical option is "back" after an imperative
  // navigation event, which becomes the new top-of-the stack
  private navDirectionHistorical: string;

  // holds restoredState nav id of current navigation. Populated whenever a popstate occurs
  private restoredStateNavId: number;

  // tracks whether currently active navigation was imperative or the result of a popstate
  private poppedState = false;

  // direction of current navigation
  // set each time a NavigationStart event occurs if triggered by popstate
  // determined by comparing destination of NavigationStart event to
  // navDirectionHistorical and navIdHistorical
  // initialized by first popstate NavigationStart
  private direction: string;

  // information needed to restore an item in the history stack
  // captured with each NavigationStart event
  private restoreState: any;
  private restoreUrl: string;

  // was cancellation invoked by a routeGuard?  If so, apply special procedures (below)
  // set and managed by each routeGuard; allows tracking to continue for any navigation
  // to preserve history, but only triggers restorative behavior when a guard is invoked
  // and navigation cancelled.
  private guardInvoked: boolean;

  // flag for track location back or forward when cancel leave page.
  // Set isRestorePopsState to true to prevent navigationEndHandler update navIdHistorical to new one when not allow leave page.
  // Location.back or location.forward in navigationCancelHandler trigger navigationEndHandler of can't deactive url routing
  // to change navIdHistorical to new navigationId, not origninal one.
  // For case Page one->Page two(CanDeactiveComponent) -> leave page two(Cancel - not leave) -> Page two(CanDeactiveComponent) loop
  private isRestorePopsState: boolean;

  constructor(private router: Router, private location: Location) {
    console.log('RouterHistoryTrackerService', 'constructor');

    this.urlCurrent = this.router.url;

    // set up subscription to router Start, End, and Cancel events for tracking and handling
    this.routerSubscription = router.events.pipe(
      filter((event: RouterEvent) => event instanceof NavigationStart || event instanceof NavigationEnd || event instanceof NavigationCancel)
    ).subscribe(event => {
      console.log('RouterHistoryTrackerService', 'event.url', event.url);

      if (event instanceof NavigationStart) {
        this.navigationStartHandler(event);
      } else if (event instanceof NavigationEnd) {
        this.navigationEndHandler(event);
      } else if (event instanceof NavigationCancel) {
        this.navigationCancelHandler(event);
      }

      console.log('RouterHistoryTrackerService', '------------------------------------------------------------------------------------');
    });
  }

  /**
  * Handler for navigationStartEvents
  * Determines direction of travel if popstate-triggered
  * Captures destination state info for use in event of cancellation
  * @param event a NavigationStart event
  */
  private navigationStartHandler(event: NavigationStart) {
    console.log('RouterHistoryTrackerService', 'navigationStartHandler');

    // popstate = back or forward clicked
    // if popstate, need to determine if it was back for forward pressed
    console.log('RouterHistoryTrackerService', 'event.navigationTrigger', event.navigationTrigger);
    console.log('RouterHistoryTrackerService', 'event.restoredState', event.restoredState);

    if (event.navigationTrigger === 'popstate') {
      this.poppedState = true;

      console.log('RouterHistoryTrackerService', 'this.restoredStateNavId', this.restoredStateNavId);

      // restored state id is part of the NavigationStart event on a popstate event; capture it
      this.restoredStateNavId = event.restoredState ? event.restoredState.navigationId : null;

      if (!this.navIdHistorical) {
        // Get restored nav id as nav id historical after page refresh
        this.navIdHistorical = this.restoredStateNavId;
      }

      // if restored state matches stored, then we're going in stored direction
      console.log('RouterHistoryTrackerService', 'this.restoredStateNavId', this.restoredStateNavId);
      console.log('RouterHistoryTrackerService', 'this.navIdHistorical', this.navIdHistorical);
      console.log('RouterHistoryTrackerService', 'this.restoredStateNavId === this.navIdHistorical', this.restoredStateNavId === this.navIdHistorical);

      console.log('RouterHistoryTrackerService', 'this.navDirectionHistorical', this.navDirectionHistorical);

      if (this.restoredStateNavId === this.navIdHistorical) {
        this.direction = this.navDirectionHistorical;
      } else {
        this.direction = this.navDirectionHistorical === 'back' ? 'forward' : 'back';
        // this.direction = 'imperative';
      }
    } else {
      this.direction = 'imperative';
    }
    console.log('RouterHistoryTrackerService', 'this.direction', this.direction);
    // on any navigation start, capture the details of the website we're starting to go to
    // in case it needs to be replaced in the stack (in the event of a popstate cancellation)
    console.log('RouterHistoryTrackerService', 'history.state', history.state);
    this.setRestoreParams(this.location.path(), history.state);

  }

  /**
   * Handler for nagiationEnd Events
   * after a completed navigation:
   * 1. sets navDirectionHistorical which is the direction to get to the last-visited page.
   * 2. sets navIdHistorical which is the navigationId of the last-visited page
   * 3. captures current page info
   *
   * @param event a NavigationEnd Event
   */
  private navigationEndHandler(event: NavigationEnd) {
    console.log('RouterHistoryTrackerService', 'navigationEndHandler');

    // if navigating imperatively, the only available direction is back because top
    // of history stack is being replaced and the prior page is now accessible by going back
    if (!this.poppedState) { // imperative navigation
      this.navDirectionHistorical = 'back';
    } else if (this.direction === 'forward') {
      this.navDirectionHistorical = 'back';
      this.poppedState = false;
    } else {
      this.navDirectionHistorical = 'forward';
      this.poppedState = false;
    }

    // capture PRIOR navigation event Id
    // (which was stored in current after last navigation end)
    // and store it for comparison on next navigation
    console.log('RouterHistoryTrackerService', 'this.navIdHistorical', this.navIdHistorical);
    if (this.isRestorePopsState) {
      this.isRestorePopsState = false;
    } else {
      this.navIdHistorical = this.navIdCurrent;
    }

    console.log('RouterHistoryTrackerService', 'this.navIdHistorical', this.navIdHistorical);

    // update current id, url, and state
    this.urlCurrent = event.url;
    this.navIdCurrent = event.id;
    this.stateCurrent = history.state;

    console.log('RouterHistoryTrackerService', 'this.urlCurrent', this.urlCurrent);
    console.log('RouterHistoryTrackerService', 'this.navIdCurrent', this.navIdCurrent);
    console.log('RouterHistoryTrackerService', 'this.stateCurrent', this.stateCurrent);

    // parse current URL into segments to allow use of router.navigate to clean up after an imperative navigation below
    this.urlCurrentSegmented = new Array<string>();
    const k = this.router.parseUrl(event.url).root.children[PRIMARY_OUTLET];
    if (k) {
      k.segments.forEach(element => this.urlCurrentSegmented.push(element.path));
    }

    console.log('RouterHistoryTrackerService', 'this.urlCurrentSegmented', this.urlCurrentSegmented);

    // after successful navigation, clear restoredId and direction
    this.clearDirectionAndRestoredId();
  }

  /**
   * handle cancellations for imperative and popstate navigation
   * @param event
   */
  private navigationCancelHandler(event: NavigationCancel) {
    console.log('RouterHistoryTrackerService', 'navigationCancelHandler');

    // early exit if cancellation was not guard-invoked (set by authGuard)
    if (!this.guardInvoked) {
      return;
    }
    /*
    Special case for an imperative navigation
    A cancelled imperative naviagtion still gets stored as the lastNavigation
    within the router (this is the rawUrl value within the transitions object in the router).
    Because cancelled imperative navigations do not change the router
    state (url never changes; replaceState never called), we do not need to adjust state.
    However having the lastNavigation set to the cancelled URL is a problem,
    if the lastNavigation is the same as what we are going back or forward to
    to (i.e. page1 -[imperative]-> page2 -[imperative]-> page1[cancel] -[back]-> page1
    Under those circumstances, the router sees the lastNavigation and the actual previous
    navigation url as the same and skips the navigation by default.
    To get around this, we fire a non-state-changing navigation to the current URL,
    which doesn't touch state, but updates the lastNavigation stored by the router to
    reflect the current URL.  Enclosed in a timeout to prevent a navigation ID mismatch error
    when the router is changed multiple times in the same cycle
    */
    if (!this.poppedState) {
      setTimeout(() => {
        this.router.navigate(this.urlCurrentSegmented, { skipLocationChange: true });
      });
    }

    /* Popstate cancellation:
    On a popstate event, state (URL) changes as soon as navigation begins.
    On a popped state cancellation, the Angular will restore the state by calling replaceState
    on the new (cancelled) URL, replacing it with the URL that we started at.
    This action breaks history because the URL we cancelled gets removed from the stack
    (clobbered by the URL we started at during the replaceState) and there are now 2 entries
    on the stack for the URL we started at (one as a result of the replaceState and the original)
    To fix the issue, we reverse the router's replaceState by calling replaceState
    with the URL we cancelled so that it is put back in the correct stack position,
    and then we roll back the popstate by navigating back (if forward was clicked)
    or forward (if back was clicked)
    */

    console.log('RouterHistoryTrackerService', 'this.poppedState', this.poppedState);
    console.log('RouterHistoryTrackerService', 'this.restoreUrl', this.restoreUrl);

    // must have a restoreUrl (set in navigationStartHandler) to proceed
    if (this.poppedState && this.restoreUrl !== undefined) {
      // reverse router's replaceState call by putting the cancelled destination URL back in the stack
      console.log('RouterHistoryTrackerService', 'history.state', history.state);

      this.location.replaceState(this.restoreUrl, undefined, this.restoreState);

      console.log('RouterHistoryTrackerService', 'history.state', history.state);
      console.log('RouterHistoryTrackerService', 'this.direction', this.direction);

      if (this.direction === 'forward') {
        this.location.back();
      } else {
        this.location.forward();
      }
      // clear restore params for next navigation
      this.resetRestoreParams();
      this.setGuardInvoked(false);

      console.log('RouterHistoryTrackerService', 'history.state', history.state);

      this.isRestorePopsState = true;
    }
    // reset popstate tracker for next navigation
    this.poppedState = false;

  }

  // getters/helper methods
  public getNavIdStored() {
    return this.navIdHistorical;
  }

  public getNavDirectionStored() {
    return this.navDirectionHistorical;
  }

  public getNavIdIdRestored() {
    return this.restoredStateNavId ? this.restoredStateNavId : null;
  }

  public getDirection() {
    return this.direction;
  }

  // called from routeGuard to invoke cancellation handler
  public setGuardInvoked(guardStatus: boolean) {
    this.guardInvoked = guardStatus;
  }

  // clear direction and restoredStateNavId for next run
  private clearDirectionAndRestoredId() {
    this.direction, this.restoredStateNavId = undefined;
  }

  // sets restore parameters for restoring stack after nav cancellation
  private setRestoreParams(restoreUrl: string, restoreState: any) {
    this.restoreUrl = restoreUrl;
    this.restoreState = restoreState;
  }

  // resets restore parameters
  private resetRestoreParams() {
    this.restoreUrl, this.restoreState = undefined;
  }

  public ngOnDestroy() {
    this.routerSubscription.unsubscribe();
  }

}
leekFreak commented 3 years ago

@lwk618, thanks. Unfortunately, that doesn't fix my problem.

pipoa commented 3 years ago

Now is 2020/07 , this issue is very important , but no solution last for 4 years?

redyaris commented 3 years ago

@leekFreak if you inject the RouterHistoryTracker in your root component, that error should not occur because urlCurrentSegmented gets initialized when the first routed component loads. I'm working on a StackBlitz demo project, which I think will be helpful, both to show the above and because there are lot of different ways to implement the canDeactivate method in a component. I'll post that here when it's done.

leekFreak commented 3 years ago

@redyaris, yep, that does it. Thank you!

baraskarharshal commented 3 years ago

I tried this workaround and I think it works. I was facing this issue in my angular app so I created a variable in one global service and stored last popped state object in it. I captured this last popped state object in "popstate" event listner as given below.

window.addEventListener('popstate', (popstateEvent) => { console.log(popstateEvent.state); this.lastPoppedState = popstateEvent.state; });

Now every time when user cancel to go away from the current page, I push this state in the history as given below.

pushLastHistoryState() { window.history.pushState(this.lastPoppedState, ''); }

Hope this will help.

aahmedayed commented 3 years ago

I tried this workaround and I think it works. I was facing this issue in my angular app so I created a variable in one global service and stored last popped state object in it. I captured this last popped state object in "popstate" event listner as given below.

window.addEventListener('popstate', (popstateEvent) => { console.log(popstateEvent.state); this.lastPoppedState = popstateEvent.state; });

Now every time when user cancel to go away from the current page, I push this state in the history as given below.

pushLastHistoryState() { window.history.pushState(this.lastPoppedState, ''); }

Hope this will help.

Thanks for this initiative @baraskarharshal. But this does not solve the real problem, read this comment https://github.com/angular/angular/issues/13586#issuecomment-637206553 from @atscott for more details.

oliverw commented 3 years ago

2020 nearing its end and this is still broken in Angular 10 11.

HighSoftWare96 commented 3 years ago

Even 2020 has no power on this bug! 😈

tuffant21 commented 3 years ago

We're rooting for you @aahmedayed ! Thank you for working on a fix!

aahmedayed commented 3 years ago

We're rooting for you @aahmedayed ! Thank you for working on a fix!

Thank you for the encouraging words, i was away for a while dealing with some personal stuff, now I’m fully back, and I promise to try my best to finish the thread ASAP.

JadJabbour commented 3 years ago

I cannot believe that such a horrendous bug has gone unfixed for 4 years.