angular: Deactivation Guard breaks the routing history
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
- Click button Nav to route1
- Click button Nav to route2
- Click button Nav to route3
- Click button Block Nav Back
- 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)
-
Angular version: 2.3.3
-
Browser: [ all ]
-
Language: [TypeScript 2.0.10 | ES5]
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 143
- Comments: 112 (16 by maintainers)
Links to this issue
Commits related to this issue
- fix(router): don't break history when CanDeactivate cancel back navigation Closes #13586 — committed to DzmitryShylovich/angular by DzmitryShylovich 7 years ago
- fix(router): don't break history when CanDeactivate cancel back navigation Closes #13586 — committed to DzmitryShylovich/angular by DzmitryShylovich 7 years ago
- fix(router): don't break history when CanDeactivate cancel back navigation Closes #13586 — committed to DzmitryShylovich/angular by DzmitryShylovich 7 years ago
- fix(router): shouldn't break history when CanDeactivate cancel back navigation Closes #13586 — committed to DzmitryShylovich/angular by DzmitryShylovich 7 years ago
- fix(router): shouldn't break history when CanDeactivate cancel back navigation Closes #13586 — committed to DzmitryShylovich/angular by DzmitryShylovich 7 years ago
- fix(router): shouldn't break history when CanDeactivate cancel back navigation Closes #13586 — committed to jasonaden/angular by DzmitryShylovich 7 years ago
- fix(router): shouldn't break history when CanDeactivate cancel back navigation Closes #13586 — committed to jasonaden/angular by DzmitryShylovich 7 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url (#37408) This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navi... — committed to angular/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url (#37408) This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navi... — committed to angular/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url (#37408) This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navi... — committed to ngwattcos/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
- fix(router): fix navigation ignoring logic to compare to the browser url This PR changes the logic for determining when to skip route processing from using the URL of the last attempted navigation to... — committed to atscott/angular by atscott 4 years ago
As a workaround, I tried to put the active url back to the history, and this approach seems to work:
I can´t believe a feature like this is broken. Lets hope 2020 is the year they fix it.
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 fromcanDeactivate()
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 likelyLocation.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. 🙁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!!
I am surprised this is still an issue. 😃 Any updates guys about this issue?
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.
Opened since 2016, I can confirm that the issue still exists in
9.0.0-next.7
, any update on this?I cannot believe that such a horrendous bug has gone unfixed for 4 years.
The known issue mentioned above was fixed in the
12.1.2
release this week. There are currently no known issues and it would be great if those interested in the fix could give it a quick test in their applications using the instructions above.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
Hi all, what’s the status on this issue? Or is there any workaround?
I guess I have to tell my client not to use the back button!
2020 nearing its end and this is still broken in Angular
1011.@jotatoledo What do you mean. The plunkr is still working and the step are still reproductible. If you update the
config.js
in the plunkr to use the latest version of Angular, you’ll see that it still is reproductible.We’re rooting for you @aahmedayed ! Thank you for working on a fix!
Any update on this issue?
The behavior of the Angular Guide’s primary router example (Crisis Center) is broken. It does not match the user-reported behavior described above, and also does not appear to match intended expected behavior. In the Crisis Center example/stackblitz:
See: https://angular.io/guide/router#routing--navigation https://angular.io/generated/live-examples/router/stackblitz.html
In my app, I don’t believe CanDeactivate guards can be used at all while this behavior is broken.
If there is an accurate workaround, can the Angular Guide (and its example) be updated to include it? Or at least, add a Known Issues section describing why users following the tutorial will not see expected behavior?
I’m facing the same behaviour here, someone know’s when it will be fixed? 😦
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 aThis wouldn’t work for SPAs in all cases except for when the back/forward is navigating to an external page.beforeUnload
listener that executes synchronouscanDeactivate
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.Still not solved? 🙄
Also chiming in that this is an issue that is affecting our app.
Our team develops an application that uses browser history heavily to navigate through pages. Unfortunately, we have faced the same issue which causes a bit weird navigation experience for users. The fix for this issue would be much appreciated!
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, andlocation.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 callingreplaceState()
again, this time with the cancelled url so it goes back on the stack in its place) and then calllocation.back()
(if the cancelled popstate was forward) orlocation.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 therawUrl
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 callingsetGuardInvoked(true)
for guarded routes. I use a global generic routeGuard service (CanDeactivateComponentService), which makes this call and then defers to thecanDeactivate()
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 calllocation.forwad()
orlocation.back()
as appropriate.router-history-tracker-service.ts
can-deactivate-component-service.ts
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.
Skaiser, its purpose is what you described, but there is a bug currently. A PR has been made to solve the issue, but until then the CanDeactivate guard breaks your history.
I couldn’t find any workaround inside my candeactivate class. Still waiting for the PR
Now is 2020/07 , this issue is very important , but no solution last for 4 years?
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 bothback
andforward
actions.popstate
before the router can respond to the navigation and this isn’t behavior we can override. This means that we always have to respond to a navigation rather than being able to truly “prevent” a deactivation/history change.back
andforward
- both of these emit apopstate
event with no distinct characteristics.back
and ignoringforward
could arguably make things worse for forward navigations (forward history gets clobbered)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.
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.
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?
Hi, quick update here:
Why hasn’t this been fixed yet? Is there any sign from the angular dev team?
Even 2020 has no power on this bug! 😈
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
’sreplaceState
andpushState
calls. So we will create a new location strategy that will extend the default path strategy (HistoryBlockingLocationStrategy
).Continuing where KevinKelchen left off.
New blocking location strategy that should be provided for the app
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?
can reproduce issue with angular 7.0.0 with canactivate
Hey Guys I am facing the same issue, can someone update, when it can be fixed. I am using angular 4.0 and router 4.0
At first sight the fix seems to work perfectly, I couldn’t reproduce a problematic case in our scenario anymore.
issue still persisting on angular router 7.2.6!!
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…
This is my workaround with a custom dialog component, based on @icesmith solution:
Guarded interface:
And I have this in my component which I want to protect(component has to implement DeactivationGuarded):
Can someone give the state of this issue?
I am on
12.2.1
and I have addedto the constructor in my
app.component.ts
.Is there anything further I need to look at or do at this time?
Any updates on this? Inquiring minds want to know…
@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.
Building a PWA today and facing this same issue…
my code
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
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:
canDeactivate
invokes per each back buttonAlso 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.
@patrickracicot @DzmitryShylovich i’m using angular 5.x and history gets lost after some by guard cancels. seems issue has not been fixed. has it?
I am also using Angular 4.0 and Router 4.0. Having this issue as well. This is a major bug and is a big setback for our application. Would be great if a workaround could be found or fix be made to this.
Can @aahmedayed say in what version this fix will be coming out? 12?
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.
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 😃
@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…
{ provide: LocationStrategy, useClass: HistoryBlockingLocationStrategy }
, in NgModuleIt’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.
Sad it’s been 3 years, but @KevinKelchen’s solution is close enough for me to use it.
@icesmith: sir in which section we have put above code i am also facing same issue?
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.
Before returning false , add this.Location.go(this.route.url) . it should fix the issue.
Issue still persists on angular version 6
I came up with the following workaround. I doesn’t fix forward button navigation (actually, it kinda makes it worse) and I’m sure there are many other problems with it. I haven’t done a lot of testing either, so I highly discourage using it, only if you’re really desperate.