I have implemented a canDeactivate guard with the common 'can I leave page' dialogue. For the most part it works. I can leave a page if I confirm or stay if I cancel. Here is the guard:
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, CanDeactivate, Router } from '@angular/router';
import { ModalComponent } from '@ccp/app/controls/layout/modal/modal.component';
import { Observable } from 'rxjs';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
}
@Injectable({
providedIn: 'root'
})
export class CanLeaveGuard implements CanDeactivate<CanComponentDeactivate> {
constructor(private router: Router) { }
canDeactivate(
component: CanComponentDeactivate,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
// the nextState is an optional url state that allows us to see the requested url
// we can use this say for example to detect if the user want to route to a previous
// page, if he does we can avoid displaying the poppin by conditionally returning true.
return component.canDeactivate ? component.canDeactivate() : true;
}
}
And here is the canDeactivate method that is called when the guard calls the method in the specified component
public shouldUserLeavePreRegFlow(dialogComponent: ModalComponent, decision: Subject<boolean>, router: Router): Observable<boolean | UrlTree> | boolean | UrlTree {
dialogComponent.open();
return decision
.pipe(map((isLeavingPage) => {
if (isLeavingPage) {
// leave page
console.log('leave page true');
return true;
} else {
// stay on current page
return router.createUrlTree([router.routerState.snapshot.url]);
}
}));
}
The decision variable is the subject navigateAwaySelection$ you see below, it does the magic of accepting an emission of true or false from my custom dialogue when it pops up and awaits for the users response, when he responds the acceptance method is called, which calls next on the subject to pass the user's decision of true or false.
public acceptance(decision: boolean): void { this.navigateAwaySelection$.next(decision); }
So this works when navigation to a typical view. But there is one edge case where we are force to click twice on the dialogue pop up when we want to confirm leave page. This case is when we try to go to a page guarded by a canActivate guard that will re-direct to a second page if on desktop, in this case we run navigateByUrl for the redirection.
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean | UrlTree {
if (this.deviceDetectorService.isDesktop()) {
this.router.navigateByUrl('register/manual',
{queryParams: route.queryParams}
);
return false;
}
return true;
}
}
The navigation tracer shows that we go to the register/manual (intended page) page, but in fact the screen flashes quickly and we are back on the dialogue and original page, clicking leave page again finally lets us go to the destination. It is like the first navigation was cancelled only to be accepted the second time, Bare in mind the canDeactivate guard is declared in the specified components route in its own feature module folder, since I am using lazy loaded module features, if I tried adding it to the global routes list the 'component' in the canDeactivate guard becomes null. For more info on this please see https://github.com/angular/angular/issues/16868.
canDeactivate guard > canActivate guard > canDeactivate guard > canActivate guard > reached intended pages.
Is there a way to fix this so that I only need to click once on leave page like in other pages?
angular tracer
// tracer result when Clicking on a link to navigate away
Router Event: NavigationStart
browser_adapter.ts:41 NavigationStart(id: 4, url: '/')
browser_adapter.ts:41 NavigationStart {id: 4, url: "/", navigationTrigger: "imperative", restoredState: null}
browser_adapter.ts:47 Router Event: RouteConfigLoadStart
browser_adapter.ts:41 RouteConfigLoadStart(path: register)
browser_adapter.ts:41 RouteConfigLoadStart {route: {…}}
logger.ts:37 Load chunk started register
browser_adapter.ts:47 Router Event: RouteConfigLoadEnd
browser_adapter.ts:41 RouteConfigLoadEnd(path: register)
browser_adapter.ts:41 RouteConfigLoadEnd {route: {…}}
logger.ts:37 Load chunk ended register
browser_adapter.ts:47 Router Event: RoutesRecognized
browser_adapter.ts:41 RoutesRecognized(id: 4, url: '/', urlAfterRedirects: '/register', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register', path:'register') { Route(url:'', path:'') } } } )
browser_adapter.ts:41 RoutesRecognized {id: 4, url: "/", urlAfterRedirects: "/register", state: RouterStateSnapshot}
browser_adapter.ts:47 Router Event: GuardsCheckStart
browser_adapter.ts:41 GuardsCheckStart(id: 4, url: '/', urlAfterRedirects: '/register', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register', path:'register') { Route(url:'', path:'') } } } )
browser_adapter.ts:41 GuardsCheckStart {id: 4, url: "/", urlAfterRedirects: "/register", state: RouterStateSnapshot}
//After clicking leave once
Router Event: ChildActivationStart
browser_adapter.ts:41 ChildActivationStart(path: '')
browser_adapter.ts:41 ChildActivationStart {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ActivationStart
browser_adapter.ts:41 ActivationStart(path: 'register')
browser_adapter.ts:41 ActivationStart {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: NavigationCancel
browser_adapter.ts:41 NavigationCancel(id: 4, url: '/')
browser_adapter.ts:41 NavigationCancel {id: 4, url: "/", reason: "Navigation ID 4 is not equal to the current navigation id 5"}
browser_adapter.ts:47 Router Event: NavigationStart
browser_adapter.ts:41 NavigationStart(id: 5, url: '/register/manual')
browser_adapter.ts:41 NavigationStart {id: 5, url: "/register/manual", navigationTrigger: "imperative", restoredState: null}
browser_adapter.ts:47 Router Event: RoutesRecognized
browser_adapter.ts:41 RoutesRecognized(id: 5, url: '/register/manual', urlAfterRedirects: '/register/manual', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register/manual', path:'register/manual') { Route(url:'', path:'') } } } )
browser_adapter.ts:41 RoutesRecognized {id: 5, url: "/register/manual", urlAfterRedirects: "/register/manual", state: RouterStateSnapshot}
browser_adapter.ts:47 Router Event: GuardsCheckStart
browser_adapter.ts:41 GuardsCheckStart(id: 5, url: '/register/manual', urlAfterRedirects: '/register/manual', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register/manual', path:'register/manual') { Route(url:'', path:'') } } } )
browser_adapter.ts:41 GuardsCheckStart {id: 5, url: "/register/manual", urlAfterRedirects: "/register/manual", state: RouterStateSnapshot}
After clicking leave a second time
Router Event: ChildActivationStart
browser_adapter.ts:41 ChildActivationStart(path: '')
browser_adapter.ts:41 ChildActivationStart {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ActivationStart
browser_adapter.ts:41 ActivationStart(path: 'register/manual')
browser_adapter.ts:41 ActivationStart {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ChildActivationStart
browser_adapter.ts:41 ChildActivationStart(path: 'register/manual')
browser_adapter.ts:41 ChildActivationStart {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ActivationStart
browser_adapter.ts:41 ActivationStart(path: '')
browser_adapter.ts:41 ActivationStart {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: GuardsCheckEnd
browser_adapter.ts:41 GuardsCheckEnd(id: 5, url: '/register/manual', urlAfterRedirects: '/register/manual', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register/manual', path:'register/manual') { Route(url:'', path:'') } } } , shouldActivate: true)
browser_adapter.ts:41 GuardsCheckEnd {id: 5, url: "/register/manual", urlAfterRedirects: "/register/manual", state: RouterStateSnapshot, shouldActivate: true}
browser_adapter.ts:47 Router Event: ResolveStart
browser_adapter.ts:41 ResolveStart(id: 5, url: '/register/manual', urlAfterRedirects: '/register/manual', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register/manual', path:'register/manual') { Route(url:'', path:'') } } } )
browser_adapter.ts:41 ResolveStart {id: 5, url: "/register/manual", urlAfterRedirects: "/register/manual", state: RouterStateSnapshot}
browser_adapter.ts:47 Router Event: ResolveEnd
browser_adapter.ts:41 ResolveEnd(id: 5, url: '/register/manual', urlAfterRedirects: '/register/manual', state: Route(url:'', path:'') { Route(url:'', path:'') { Route(url:'register/manual', path:'register/manual') { Route(url:'', path:'') } } } )
browser_adapter.ts:41 ResolveEnd {id: 5, url: "/register/manual", urlAfterRedirects: "/register/manual", state: RouterStateSnapshot}
browser_adapter.ts:47 Router Event: ActivationEnd
browser_adapter.ts:41 ActivationEnd(path: '')
browser_adapter.ts:41 ActivationEnd {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ChildActivationEnd
browser_adapter.ts:41 ChildActivationEnd(path: 'register/manual')
browser_adapter.ts:41 ChildActivationEnd {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ActivationEnd
browser_adapter.ts:41 ActivationEnd(path: 'register/manual')
browser_adapter.ts:41 ActivationEnd {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ChildActivationEnd
browser_adapter.ts:41 ChildActivationEnd(path: '')
browser_adapter.ts:41 ChildActivationEnd {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ActivationEnd
browser_adapter.ts:41 ActivationEnd(path: '')
browser_adapter.ts:41 ActivationEnd {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: ChildActivationEnd
browser_adapter.ts:41 ChildActivationEnd(path: '')
browser_adapter.ts:41 ChildActivationEnd {snapshot: ActivatedRouteSnapshot}
browser_adapter.ts:47 Router Event: NavigationEnd
browser_adapter.ts:41 NavigationEnd(id: 5, url: '/register/manual', urlAfterRedirects: '/register/manual')
browser_adapter.ts:41 NavigationEnd {id: 5, url: "/register/manual", urlAfterRedirects: "/register/manual"}
maybe this is the culprit:
browser_adapter.ts:47 Router Event: NavigationCancel
browser_adapter.ts:41 NavigationCancel(id: 4, url: '/')
browser_adapter.ts:41 NavigationCancel {id: 4, url: "/", reason: "Navigation ID 4 is not equal to the current navigation id 5"}