Skip to content

Commit 8735af0

Browse files
committed
feat(router): Add ability to return UrlTree with NavigationBehaviorOptions from guards (#45023)
Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not have feature-parity with the old `router.navigate(...); return false;` pattern. The most common use-case for this feature is to redirect to a new page _without_ updating the URL from the initially attempted navigation. For example, rendering a 404 page when the user does not have access privelages to a route. Fixes #17004 Fixes #27148 BREAKING CHANGE: Guards can now return `RedirectCommand` for redirects in addition to `UrlTree`. Code which expects only `boolean` or `UrlTree` values in `Route` types will need to be adjusted. PR Close #45023
1 parent 87cae55 commit 8735af0

13 files changed

Lines changed: 124 additions & 26 deletions

File tree

‎adev/src/content/guide/routing/router-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ These events are shown in the following table.
170170
| [`ChildActivationEnd`](api/router/ChildActivationEnd) | Triggered when the Router finishes activating a route's children. |
171171
| [`ActivationEnd`](api/router/ActivationEnd) | Triggered when the Router finishes activating a route. |
172172
| [`NavigationEnd`](api/router/NavigationEnd) | Triggered when navigation ends successfully. |
173-
| [`NavigationCancel`](api/router/NavigationCancel) | Triggered when navigation is canceled. This can happen when a Route Guard returns false during navigation, or redirects by returning a `UrlTree`. |
173+
| [`NavigationCancel`](api/router/NavigationCancel) | Triggered when navigation is canceled. This can happen when a Route Guard returns false during navigation, or redirects by returning a `UrlTree` or `RedirectCommand`. |
174174
| [`NavigationError`](api/router/NavigationError) | Triggered when navigation fails due to an unexpected error. |
175175
| [`Scroll`](api/router/Scroll) | Represents a scrolling event. |
176176

‎goldens/public-api/router/index.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti
306306
}
307307

308308
// @public
309-
export type GuardResult = boolean | UrlTree;
309+
export type GuardResult = boolean | UrlTree | RedirectCommand;
310310

311311
// @public
312312
export class GuardsCheckEnd extends RouterEvent {
@@ -595,6 +595,15 @@ export function provideRoutes(routes: Routes): Provider[];
595595
// @public
596596
export type QueryParamsHandling = 'merge' | 'preserve' | '';
597597

598+
// @public
599+
export class RedirectCommand {
600+
constructor(redirectTo: UrlTree, navigationBehaviorOptions?: NavigationBehaviorOptions | undefined);
601+
// (undocumented)
602+
readonly navigationBehaviorOptions?: NavigationBehaviorOptions | undefined;
603+
// (undocumented)
604+
readonly redirectTo: UrlTree;
605+
}
606+
598607
// @public @deprecated
599608
export interface Resolve<T> {
600609
// (undocumented)

‎packages/core/test/bundling/router/bundle.golden_symbols.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,9 @@
557557
{
558558
"name": "Recognizer"
559559
},
560+
{
561+
"name": "RedirectCommand"
562+
},
560563
{
561564
"name": "RedirectRequest"
562565
},

‎packages/router/src/events.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Route} from './models';
9+
import {NavigationBehaviorOptions, Route} from './models';
1010
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
1111
import {UrlTree} from './url_tree';
1212

@@ -611,7 +611,10 @@ export class Scroll {
611611

612612
export class BeforeActivateRoutes {}
613613
export class RedirectRequest {
614-
constructor(readonly url: UrlTree) {}
614+
constructor(
615+
readonly url: UrlTree,
616+
readonly navigationBehaviorOptions: NavigationBehaviorOptions | undefined,
617+
) {}
615618
}
616619
export type PrivateRouterEvents = BeforeActivateRoutes | RedirectRequest;
617620

‎packages/router/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export {
5656
RunGuardsAndResolvers,
5757
UrlMatcher,
5858
UrlMatchResult,
59+
RedirectCommand,
5960
} from './models';
6061
export {ViewTransitionInfo, ViewTransitionsFeatureOptions} from './utils/view_transition';
6162

‎packages/router/src/models.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,23 @@ export type DeprecatedGuard = ProviderToken<any> | any;
7070
* @see [Routing tutorial](guide/router-tutorial-toh#milestone-5-route-guards)
7171
* @publicApi
7272
*/
73-
export type GuardResult = boolean | UrlTree;
73+
export type GuardResult = boolean | UrlTree | RedirectCommand;
74+
75+
/**
76+
* Can be returned by a `Router` guard to instruct the `Router` to redirect rather than continue
77+
* processing the path of the in-flight navigation. The `redirectTo` indicates _where_ the new
78+
* navigation should go to and the optional `navigationBehaviorOptions` can provide more information
79+
* about _how_ to perform the navigation.
80+
*
81+
* @see [Routing tutorial](guide/router-tutorial-toh#milestone-5-route-guards)
82+
* @publicApi
83+
*/
84+
export class RedirectCommand {
85+
constructor(
86+
readonly redirectTo: UrlTree,
87+
readonly navigationBehaviorOptions?: NavigationBehaviorOptions,
88+
) {}
89+
}
7490

7591
/**
7692
* Type used to represent a value which may be synchronous or async.

‎packages/router/src/navigation_canceling_error.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {NavigationCancellationCode} from './events';
10-
import {NavigationBehaviorOptions} from './models';
10+
import {NavigationBehaviorOptions, RedirectCommand} from './models';
1111
import {isUrlTree, UrlSerializer, UrlTree} from './url_tree';
1212

1313
export const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';
@@ -24,7 +24,7 @@ export type RedirectingNavigationCancelingError = NavigationCancelingError & {
2424

2525
export function redirectingNavigationError(
2626
urlSerializer: UrlSerializer,
27-
redirect: UrlTree,
27+
redirect: UrlTree | RedirectCommand,
2828
): RedirectingNavigationCancelingError {
2929
const {redirectTo, navigationBehaviorOptions} = isUrlTree(redirect)
3030
? {redirectTo: redirect, navigationBehaviorOptions: undefined}

‎packages/router/src/navigation_transition.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
RouteConfigLoadStart,
4545
RoutesRecognized,
4646
} from './events';
47-
import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
47+
import {GuardResult, NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
4848
import {
4949
isNavigationCancelingError,
5050
isRedirectingNavigationCancelingError,
@@ -297,7 +297,7 @@ export interface NavigationTransition {
297297
currentRouterState: RouterState;
298298
targetRouterState: RouterState | null;
299299
guards: Checks;
300-
guardsResult: boolean | UrlTree | null;
300+
guardsResult: GuardResult | null;
301301
}
302302

303303
/**
@@ -608,7 +608,7 @@ export class NavigationTransitions {
608608
checkGuards(this.environmentInjector, (evt: Event) => this.events.next(evt)),
609609
tap((t) => {
610610
overallTransitionState.guardsResult = t.guardsResult;
611-
if (isUrlTree(t.guardsResult)) {
611+
if (t.guardsResult && typeof t.guardsResult !== 'boolean') {
612612
throw redirectingNavigationError(this.urlSerializer, t.guardsResult);
613613
}
614614

@@ -824,7 +824,7 @@ export class NavigationTransitions {
824824
if (!isRedirectingNavigationCancelingError(e)) {
825825
overallTransitionState.resolve(false);
826826
} else {
827-
this.events.next(new RedirectRequest(e.url));
827+
this.events.next(new RedirectRequest(e.url, e.navigationBehaviorOptions));
828828
}
829829

830830
/* All other errors should reset to the router's internal URL reference

‎packages/router/src/operators/check_guards.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
CanMatchFn,
3030
Route,
3131
} from '../models';
32-
import {redirectingNavigationError} from '../navigation_canceling_error';
32+
import {navigationCancelingError, redirectingNavigationError} from '../navigation_canceling_error';
3333
import {NavigationTransition} from '../navigation_transition';
3434
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
3535
import {isUrlTree, UrlSegment, UrlSerializer, UrlTree} from '../url_tree';
@@ -260,12 +260,10 @@ export function runCanLoadGuards(
260260
return of(canLoadObservables).pipe(prioritizedGuardValue(), redirectIfUrlTree(urlSerializer));
261261
}
262262

263-
function redirectIfUrlTree(
264-
urlSerializer: UrlSerializer,
265-
): OperatorFunction<UrlTree | boolean, boolean> {
263+
function redirectIfUrlTree(urlSerializer: UrlSerializer): OperatorFunction<GuardResult, boolean> {
266264
return pipe(
267-
tap((result: UrlTree | boolean) => {
268-
if (!isUrlTree(result)) return;
265+
tap((result: GuardResult) => {
266+
if (typeof result === 'boolean') return;
269267

270268
throw redirectingNavigationError(urlSerializer, result);
271269
}),
@@ -278,7 +276,7 @@ export function runCanMatchGuards(
278276
route: Route,
279277
segments: UrlSegment[],
280278
urlSerializer: UrlSerializer,
281-
): Observable<boolean> {
279+
): Observable<GuardResult> {
282280
const canMatch = route.canMatch;
283281
if (!canMatch || canMatch.length === 0) return of(true);
284282

‎packages/router/src/operators/prioritized_guard_value.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
import {combineLatest, Observable, OperatorFunction} from 'rxjs';
1010
import {filter, map, startWith, switchMap, take} from 'rxjs/operators';
1111

12-
import {GuardResult} from '../models';
13-
import {UrlTree} from '../url_tree';
12+
import {GuardResult, RedirectCommand} from '../models';
13+
import {isUrlTree, UrlTree} from '../url_tree';
1414

1515
const INITIAL_VALUE = /* @__PURE__ */ Symbol('INITIAL_VALUE');
16-
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree;
16+
declare type INTERIM_VALUES = typeof INITIAL_VALUE | GuardResult;
1717

1818
export function prioritizedGuardValue(): OperatorFunction<Observable<GuardResult>[], GuardResult> {
1919
return switchMap((obs) => {
@@ -28,9 +28,9 @@ export function prioritizedGuardValue(): OperatorFunction<Observable<GuardResult
2828
} else if (result === INITIAL_VALUE) {
2929
// If guard has not finished, we need to stop processing.
3030
return INITIAL_VALUE;
31-
} else if (result === false || result instanceof UrlTree) {
31+
} else if (result === false || isRedirect(result)) {
3232
// Result finished and was not true. Return the result.
33-
// Note that we only allow false/UrlTree. Other values are considered invalid and
33+
// Note that we only allow false/UrlTree/RedirectCommand. Other values are considered invalid and
3434
// ignored.
3535
return result;
3636
}
@@ -43,3 +43,7 @@ export function prioritizedGuardValue(): OperatorFunction<Observable<GuardResult
4343
);
4444
});
4545
}
46+
47+
function isRedirect(val: INTERIM_VALUES): val is UrlTree | RedirectCommand {
48+
return isUrlTree(val) || val instanceof RedirectCommand;
49+
}

0 commit comments

Comments
 (0)