diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index 07aa043ac0f..8905f7d4b70 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -636,7 +636,7 @@ export class ResolveEnd extends RouterEvent { } // @public -export type ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync; +export type ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync; // @public export class ResolveStart extends RouterEvent { diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index fcaace7e6cc..36df23ffb60 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -165,6 +165,11 @@ export type Data = { * * Represents the resolved data associated with a particular route. * + * Returning a `RedirectCommand` directs the router to cancel the current navigation and redirect to + * the location provided in the `RedirectCommand`. Note that there are no ordering guarantees when + * resolvers execute. If multiple resolvers would return a `RedirectCommand`, only the first one + * returned will be used. + * * @see {@link Route#resolve} * * @publicApi @@ -1221,7 +1226,7 @@ export interface Resolve { export type ResolveFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, -) => MaybeAsync; +) => MaybeAsync; /** * @description diff --git a/packages/router/src/operators/resolve_data.ts b/packages/router/src/operators/resolve_data.ts index 77d5e153f77..38848891fc4 100644 --- a/packages/router/src/operators/resolve_data.ts +++ b/packages/router/src/operators/resolve_data.ts @@ -10,7 +10,7 @@ import {EnvironmentInjector, ProviderToken, runInInjectionContext} from '@angula import {EMPTY, from, MonoTypeOperatorFunction, Observable, of, throwError} from 'rxjs'; import {catchError, concatMap, first, map, mapTo, mergeMap, takeLast, tap} from 'rxjs/operators'; -import {ResolveData} from '../models'; +import {RedirectCommand, ResolveData} from '../models'; import {NavigationTransition} from '../navigation_transition'; import { ActivatedRouteSnapshot, @@ -23,6 +23,8 @@ import {getDataKeys, wrapIntoObservable} from '../utils/collection'; import {getClosestRouteInjector} from '../utils/config'; import {getTokenOrFunctionIdentity} from '../utils/preactivation'; import {isEmptyError} from '../utils/type_guards'; +import {redirectingNavigationError} from '../navigation_canceling_error'; +import {DefaultUrlSerializer} from '../url_tree'; export function resolveData( paramsInheritanceStrategy: 'emptyOnly' | 'always', @@ -112,6 +114,9 @@ function resolveNode( getResolver(resolve[key], futureARS, futureRSS, injector).pipe( first(), tap((value: any) => { + if (value instanceof RedirectCommand) { + throw redirectingNavigationError(new DefaultUrlSerializer(), value); + } data[key] = value; }), ), diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index f708b9aca9d..c11bbb99ce5 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -2432,6 +2432,27 @@ for (const browserAPI of ['navigation', 'history'] as const) { }), )); + it('should redirect if a resolver returns RedirectCommand', fakeAsync(() => { + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmpWithTwoOutlets); + + router.resetConfig([ + { + path: 'parent/:id', + component: BlankCmp, + resolve: {redirectMe: () => new RedirectCommand(router.parseUrl('/login'))}, + }, + { + path: 'login', + component: BlankCmp, + }, + ]); + + router.navigateByUrl('/parent/1'); + advance(fixture); + expect(router.url).toEqual('/login'); + })); + it('should handle errors', fakeAsync( inject([Router], (router: Router) => { const fixture = createRoot(router, RootCmp);