From 6c1357dd7d5f466575398ebc32c581068d9bf2df Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 27 Jun 2022 10:48:30 -0700 Subject: [PATCH] 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 --- goldens/public-api/router/index.md | 13 ++++++++-- packages/router/src/events.ts | 38 ++++++++++++++++++++++++++-- packages/router/src/index.ts | 2 +- packages/router/src/router.ts | 31 +++++++++++++++-------- packages/router/src/router_module.ts | 15 ++++------- 5 files changed, 73 insertions(+), 26 deletions(-) diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index 1b2c2179e53..979d53735c4 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -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( diff --git a/packages/router/src/events.ts b/packages/router/src/events.ts index d6a58d65965..532643408c8 100644 --- a/packages/router/src/events.ts +++ b/packages/router/src/events.ts @@ -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); } diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 9a5f233526c..18f2bad0c24 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -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'; diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 699a95c0796..dbc529113e6 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -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); } diff --git a/packages/router/src/router_module.ts b/packages/router/src/router_module.ts index 64b6de0479b..c02ca03e21f 100644 --- a/packages/router/src/router_module.ts +++ b/packages/router/src/router_module.ts @@ -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),