feat(router): Add stable cancelation code to NavigationCancel event (#46675)

This commit adds a stable cancelation code to the `NavigationCancel`
event. This code is acceptable for use in production whereas parsing the
`reason` string is not. This allows developers to determine more
specifically _why_ a navigation was canceled and perform different
actions in different scenarios.

PR Close #46675
This commit is contained in:
Andrew Scott
2022-06-27 10:48:30 -07:00
committed by Andrew Scott
parent e96ece901c
commit 6c1357dd7d
5 changed files with 73 additions and 26 deletions
+11 -2
View File
@@ -351,8 +351,9 @@ export class NavigationCancel extends RouterEvent {
constructor(
id: number,
url: string,
reason: string);
// (undocumented)
reason: string,
code?: NavigationCancellationCode | undefined);
readonly code?: NavigationCancellationCode | undefined;
reason: string;
// (undocumented)
toString(): string;
@@ -360,6 +361,14 @@ export class NavigationCancel extends RouterEvent {
readonly type = EventType.NavigationCancel;
}
// @public
export const enum NavigationCancellationCode {
GuardRejected = 3,
NoDataFromResolver = 2,
Redirect = 0,
SupersededByNewNavigation = 1
}
// @public
export class NavigationEnd extends RouterEvent {
constructor(
+36 -2
View File
@@ -162,6 +162,31 @@ export class NavigationEnd extends RouterEvent {
}
}
/**
* A code for the `NavigationCancel` event of the `Router` to indicate the
* reason a navigation failed.
*
* @publicApi
*/
export const enum NavigationCancellationCode {
/**
* A navigation failed because a guard returned a `UrlTree` to redirect.
*/
Redirect,
/**
* A navigation failed because a more recent navigation started.
*/
SupersededByNewNavigation,
/**
* A navigation failed because one of the resolvers completed without emiting a value.
*/
NoDataFromResolver,
/**
* A navigation failed because a guard returned `false`.
*/
GuardRejected,
}
/**
* An event triggered when a navigation is canceled, directly or indirectly.
* This can happen for several reasons including when a route guard
@@ -181,8 +206,17 @@ export class NavigationCancel extends RouterEvent {
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public reason: string) {
/**
* A description of why the navigation was cancelled. For debug purposes only. Use `code`
* instead for a stable cancellation reason that can be used in production.
*/
public reason: string,
/**
* A code to indicate why the navigation was canceled. This cancellation code is stable for
* the reason and can be relied on whereas the `reason` string could change and should not be
* used in production.
*/
readonly code?: NavigationCancellationCode) {
super(id, url);
}
+1 -1
View File
@@ -11,7 +11,7 @@ export {createUrlTreeFromSnapshot} from './create_url_tree';
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode as NavigationCancellationCode, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, CanMatch, CanMatchFn, Data, LoadChildren, LoadChildrenCallback, QueryParamsHandling, Resolve, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
+20 -11
View File
@@ -14,7 +14,7 @@ import {catchError, defaultIfEmpty, filter, finalize, map, switchMap, take, tap}
import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree';
import {RuntimeErrorCode} from './errors';
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {QueryParamsHandling, Route, Routes} from './models';
import {activateRoutes} from './operators/activate_routes';
import {applyRedirects} from './operators/apply_redirects';
@@ -411,8 +411,7 @@ export class Router {
private disposed = false;
private locationSubscription?: SubscriptionLike;
/** @internal */
navigationId: number = 0;
private navigationId: number = 0;
/**
* The id of the currently active page in the router.
@@ -779,7 +778,8 @@ export class Router {
filter(t => {
if (!t.guardsResult) {
this.restoreHistory(t);
this.cancelNavigationTransition(t, '');
this.cancelNavigationTransition(
t, '', NavigationCancellationCode.GuardRejected);
return false;
}
return true;
@@ -807,7 +807,10 @@ export class Router {
this.restoreHistory(t);
this.cancelNavigationTransition(
t,
`At least one route resolver didn't emit any value.`);
NG_DEV_MODE ?
`At least one route resolver didn't emit any value.` :
'',
NavigationCancellationCode.NoDataFromResolver);
}
}
}),
@@ -896,9 +899,13 @@ export class Router {
* event is fired when a navigation gets cancelled but not caught by other
* means. */
if (!completed && !errored) {
const cancelationReason = `Navigation ID ${
t.id} is not equal to the current navigation id ${this.navigationId}`;
this.cancelNavigationTransition(t, cancelationReason);
const cancelationReason = NG_DEV_MODE ?
`Navigation ID ${t.id} is not equal to the current navigation id ${
this.navigationId}` :
'';
this.cancelNavigationTransition(
t, cancelationReason,
NavigationCancellationCode.SupersededByNewNavigation);
}
// Only clear current navigation if it is still set to the one that
// finalized.
@@ -933,7 +940,8 @@ export class Router {
this.restoreHistory(t, true);
}
const navCancel = new NavigationCancel(
t.id, this.serializeUrl(t.extractedUrl), e.message);
t.id, this.serializeUrl(t.extractedUrl), e.message,
NavigationCancellationCode.Redirect);
eventsSubject.next(navCancel);
// When redirecting, we need to delay resolving the navigation
@@ -1460,8 +1468,9 @@ export class Router {
this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId));
}
private cancelNavigationTransition(t: NavigationTransition, reason: string) {
const navCancel = new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), reason);
private cancelNavigationTransition(
t: NavigationTransition, reason: string, code: NavigationCancellationCode) {
const navCancel = new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), reason, code);
this.triggerEvent(navCancel);
t.resolve(false);
}
+5 -10
View File
@@ -16,7 +16,7 @@ import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active';
import {RouterOutlet} from './directives/router_outlet';
import {RuntimeErrorCode} from './errors';
import {Event, NavigationCancel, NavigationEnd, NavigationError, stringifyEvent} from './events';
import {Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, stringifyEvent} from './events';
import {Route, Routes} from './models';
import {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
import {RouteReuseStrategy} from './route_reuse_strategy';
@@ -26,7 +26,6 @@ import {ChildrenOutletContexts} from './router_outlet_context';
import {PreloadingStrategy, RouterPreloader} from './router_preloader';
import {ROUTER_SCROLLER, RouterScroller} from './router_scroller';
import {ActivatedRoute} from './router_state';
import {REDIRECTING_CANCELLATION_REASON} from './shared';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from './url_tree';
import {flatten} from './utils/collection';
@@ -611,15 +610,11 @@ function provideEnabledBlockingInitialNavigation(): Provider {
// Navigation assumed to succeed if we get `ActivationStart`
return true;
}
const newNavigationStarted = router.navigationId !== e.id;
// TODO(atscott): Do not rely on the string reason to determine if cancelation
// is redirecting
const redirectingWithUrlTree = e instanceof NavigationCancel ?
e.reason.indexOf(REDIRECTING_CANCELLATION_REASON) !== -1 :
const redirecting = e instanceof NavigationCancel ?
(e.code === NavigationCancellationCode.Redirect ||
e.code === NavigationCancellationCode.SupersededByNewNavigation) :
false;
// Navigation failed, but if we already have a new navigation, wait for the
// result of that one instead.
return newNavigationStarted || redirectingWithUrlTree ? null : false;
return redirecting ? null : false;
}),
filter((result): result is boolean => result !== null),
take(1),