Skip to content

Commit

Permalink
feat(router): Allow resolvers to return RedirectCommand (#54556)
Browse files Browse the repository at this point in the history
Returning a `RedirectCommand` from a resolver can be interpreted as
distinctly different from regular resolved data. When a resolver returns
`RedirectCommand` we can interperet this as an intention to redirect in
the same way as other guards.

resolves #29089

PR Close #54556
  • Loading branch information
atscott committed Mar 30, 2024
1 parent 8a8181a commit 87f3f27
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 3 deletions.
2 changes: 1 addition & 1 deletion goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ export class ResolveEnd extends RouterEvent {
}

// @public
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<T>;
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<T | RedirectCommand>;

// @public
export class ResolveStart extends RouterEvent {
Expand Down
7 changes: 6 additions & 1 deletion packages/router/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1221,7 +1226,7 @@ export interface Resolve<T> {
export type ResolveFn<T> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => MaybeAsync<T>;
) => MaybeAsync<T | RedirectCommand>;

/**
* @description
Expand Down
7 changes: 6 additions & 1 deletion packages/router/src/operators/resolve_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -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;
}),
),
Expand Down
21 changes: 21 additions & 0 deletions packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 87f3f27

Please sign in to comment.