From 31b94c762fc91ab6cabe08ea6812780fdcf92a64 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 20 Dec 2022 08:35:23 -0800 Subject: [PATCH] feat(router): Add a withNavigationErrorHandler feature to provideRouter (#48551) `withNavigationErrorHandler` is a close replacement for the `RouterModule.forRoot.errorHandler` / `Router.errorHandler`. It provides a quick, short way for users to define a function to handle `NavigationError` events. PR Close #48551 --- aio/content/guide/deprecations.md | 3 +- goldens/public-api/router/index.md | 8 ++- packages/router/src/index.ts | 2 +- packages/router/src/provide_router.ts | 64 ++++++++++++++++++++++-- packages/router/test/integration.spec.ts | 25 ++++++++- 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index eda05f4af2d..4db27e1222d 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -396,13 +396,12 @@ available in `RouterModule.forRoot` or `provideRouter` and `withRouterConfig`. * `paramsInheritanceStrategy` * `urlUpdateStrategy` * `canceledNavigationResolution` +* `errorHandler` The following options are available in `RouterModule.forRoot` but not available in `provideRouter`: * `malformedUriErrorHandler` - This was not found to be used by anyone. There are currently no plans to make this available in `provideRouter`. -* `errorHandler` - Developers should instead subscribe to `Router.events` - and filter for `NavigationError`. diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index 265c3388cd8..48304d17823 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -246,6 +246,9 @@ export type DisabledInitialNavigationFeature = RouterFeature; +// @public +export type ErrorHandlerFeature = RouterFeature; + // @public type Event_2 = RouterEvent | NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | GuardsCheckStart | GuardsCheckEnd | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd | Scroll | ResolveStart | ResolveEnd | NavigationSkipped; export { Event_2 as Event } @@ -748,7 +751,7 @@ export interface RouterFeature { } // @public -export type RouterFeatures = PreloadingFeature | DebugTracingFeature | InitialNavigationFeature | InMemoryScrollingFeature | RouterConfigurationFeature; +export type RouterFeatures = PreloadingFeature | DebugTracingFeature | InitialNavigationFeature | InMemoryScrollingFeature | RouterConfigurationFeature | ErrorHandlerFeature; // @public export type RouterHashLocationFeature = RouterFeature; @@ -1071,6 +1074,9 @@ export function withDisabledInitialNavigation(): DisabledInitialNavigationFeatur // @public export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNavigationFeature; +// @public +export function withErrorHandler(fn: (error: NavigationError) => void): ErrorHandlerFeature; + // @public export function withHashLocation(): RouterConfigurationFeature; diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index fd07f070df1..b7bf6815ddf 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -15,7 +15,7 @@ export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart export {CanActivate, CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivate, CanDeactivateFn, CanLoad, CanLoadFn, CanMatch, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, Resolve, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models'; export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition'; export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy'; -export {DebugTracingFeature, DisabledInitialNavigationFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, RouterHashLocationFeature, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withHashLocation, withInMemoryScrolling, withPreloading, withRouterConfig} from './provide_router'; +export {DebugTracingFeature, DisabledInitialNavigationFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, NavigationErrorHandlerFeature as ErrorHandlerFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, RouterHashLocationFeature, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withHashLocation, withInMemoryScrolling, withNavigationErrorHandler as withErrorHandler, withPreloading, withRouterConfig} from './provide_router'; export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy'; export {Router} from './router'; export {ExtraOptions, InitialNavigation, InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config'; diff --git a/packages/router/src/provide_router.ts b/packages/router/src/provide_router.ts index 9836cd17ca1..d98be8d8fc4 100644 --- a/packages/router/src/provide_router.ts +++ b/packages/router/src/provide_router.ts @@ -7,7 +7,7 @@ */ import {HashLocationStrategy, LOCATION_INITIALIZED, LocationStrategy, ViewportScroller} from '@angular/common'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, NgZone, Provider, Type} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, NgZone, Provider, Type} from '@angular/core'; import {of, Subject} from 'rxjs'; import {filter, map, take} from 'rxjs/operators'; @@ -621,6 +621,63 @@ export function withHashLocation(): RouterConfigurationFeature { return routerFeature(RouterFeatureKind.RouterConfigurationFeature, providers); } +/** + * A type alias for providers returned by `withNavigationErrorHandler` for use with `provideRouter`. + * + * @see `withNavigationErrorHandler` + * @see `provideRouter` + * + * @publicApi + */ +export type NavigationErrorHandlerFeature = + RouterFeature; + +/** + * Subscribes to the Router's navigation events and calls the given function when a + * `NavigationError` happens. + * + * This function is run inside application's injection context so you can use the `inject` function. + * + * @usageNotes + * + * Basic example of how you can use the error handler option: + * ``` + * const appRoutes: Routes = []; + * bootstrapApplication(AppComponent, + * { + * providers: [ + * provideRouter(appRoutes, withNavigationErrorHandler((e: NavigationError) => + * inject(MyErrorTracker).trackError(e)) + * ] + * } + * ); + * ``` + * + * @see `NavigationError` + * @see `inject` + * @see `EnvironmentInjector#runInContext` + * + * @returns A set of providers for use with `provideRouter`. + * + * @publicApi + */ +export function withNavigationErrorHandler(fn: (error: NavigationError) => void): + NavigationErrorHandlerFeature { + const providers = [{ + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => { + const injector = inject(EnvironmentInjector); + inject(Router).events.subscribe((e) => { + if (e instanceof NavigationError) { + injector.runInContext(() => fn(e)); + } + }); + } + }]; + return routerFeature(RouterFeatureKind.NavigationErrorHandlerFeature, providers); +} + /** * A type alias that represents all Router features available for use with `provideRouter`. * Features can be enabled by adding special functions to the `provideRouter` call. @@ -632,7 +689,7 @@ export function withHashLocation(): RouterConfigurationFeature { * @publicApi */ export type RouterFeatures = PreloadingFeature|DebugTracingFeature|InitialNavigationFeature| - InMemoryScrollingFeature|RouterConfigurationFeature; + InMemoryScrollingFeature|RouterConfigurationFeature|NavigationErrorHandlerFeature; /** * The list of features as an enum to uniquely type each feature. @@ -644,5 +701,6 @@ export const enum RouterFeatureKind { DisabledInitialNavigationFeature, InMemoryScrollingFeature, RouterConfigurationFeature, - RouterHashLocationFeature + RouterHashLocationFeature, + NavigationErrorHandlerFeature, } diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index fd00d3a91b0..8236fa9f6cd 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -17,7 +17,7 @@ import {concat, EMPTY, Observable, Observer, of, Subscription} from 'rxjs'; import {delay, filter, first, last, map, mapTo, takeWhile, tap} from 'rxjs/operators'; import {CanActivateChildFn, CanActivateFn, CanMatch, CanMatchFn, Data, ResolveFn} from '../src/models'; -import {provideRouter, withRouterConfig} from '../src/provide_router'; +import {provideRouter, withNavigationErrorHandler, withRouterConfig} from '../src/provide_router'; import {forEach, wrapIntoObservable} from '../src/utils/collection'; import {getLoadedRoutes} from '../src/utils/config'; @@ -1668,6 +1668,29 @@ describe('Integration', () => { ]); }))); + it('should be able to provide an error handler with DI dependencies', async () => { + @Injectable({providedIn: 'root'}) + class Handler { + handlerCalled = false; + } + TestBed.configureTestingModule({ + providers: [ + provideRouter( + [{ + path: 'throw', + canMatch: [() => { + throw new Error(''); + }], + component: BlankCmp + }], + withNavigationErrorHandler(() => coreInject(Handler).handlerCalled = true)), + ] + }); + const router = TestBed.inject(Router); + await expectAsync(router.navigateByUrl('/throw')).toBeRejected(); + expect(TestBed.inject(Handler).handlerCalled).toBeTrue(); + }); + // Errors should behave the same for both deferred and eager URL update strategies ['deferred', 'eager'].forEach((strat: any) => { it('should dispatch NavigationError after the url has been reset back', fakeAsync(() => {