mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
8735af08b9
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
922 lines
34 KiB
TypeScript
922 lines
34 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {Location} from '@angular/common';
|
|
import {EnvironmentInjector, inject, Injectable, Type} from '@angular/core';
|
|
import {BehaviorSubject, combineLatest, EMPTY, from, Observable, of, Subject} from 'rxjs';
|
|
import {
|
|
catchError,
|
|
defaultIfEmpty,
|
|
filter,
|
|
finalize,
|
|
map,
|
|
switchMap,
|
|
take,
|
|
takeUntil,
|
|
tap,
|
|
} from 'rxjs/operators';
|
|
|
|
import {createRouterState} from './create_router_state';
|
|
import {INPUT_BINDER} from './directives/router_outlet';
|
|
import {
|
|
BeforeActivateRoutes,
|
|
Event,
|
|
GuardsCheckEnd,
|
|
GuardsCheckStart,
|
|
IMPERATIVE_NAVIGATION,
|
|
NavigationCancel,
|
|
NavigationCancellationCode,
|
|
NavigationEnd,
|
|
NavigationError,
|
|
NavigationSkipped,
|
|
NavigationSkippedCode,
|
|
NavigationStart,
|
|
NavigationTrigger,
|
|
RedirectRequest,
|
|
ResolveEnd,
|
|
ResolveStart,
|
|
RouteConfigLoadEnd,
|
|
RouteConfigLoadStart,
|
|
RoutesRecognized,
|
|
} from './events';
|
|
import {GuardResult, NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
|
|
import {
|
|
isNavigationCancelingError,
|
|
isRedirectingNavigationCancelingError,
|
|
redirectingNavigationError,
|
|
} from './navigation_canceling_error';
|
|
import {activateRoutes} from './operators/activate_routes';
|
|
import {checkGuards} from './operators/check_guards';
|
|
import {recognize} from './operators/recognize';
|
|
import {resolveData} from './operators/resolve_data';
|
|
import {switchTap} from './operators/switch_tap';
|
|
import {TitleStrategy} from './page_title_strategy';
|
|
import {RouteReuseStrategy} from './route_reuse_strategy';
|
|
import {ROUTER_CONFIGURATION} from './router_config';
|
|
import {RouterConfigLoader} from './router_config_loader';
|
|
import {ChildrenOutletContexts} from './router_outlet_context';
|
|
import {
|
|
ActivatedRoute,
|
|
ActivatedRouteSnapshot,
|
|
createEmptyState,
|
|
RouterState,
|
|
RouterStateSnapshot,
|
|
} from './router_state';
|
|
import {Params} from './shared';
|
|
import {UrlHandlingStrategy} from './url_handling_strategy';
|
|
import {isUrlTree, UrlSerializer, UrlTree} from './url_tree';
|
|
import {Checks, getAllRouteGuards} from './utils/preactivation';
|
|
import {CREATE_VIEW_TRANSITION} from './utils/view_transition';
|
|
|
|
/**
|
|
* @description
|
|
*
|
|
* Options that modify the `Router` URL.
|
|
* Supply an object containing any of these properties to a `Router` navigation function to
|
|
* control how the target URL should be constructed.
|
|
*
|
|
* @see {@link Router#navigate}
|
|
* @see {@link Router#createUrlTree}
|
|
* @see [Routing and Navigation guide](guide/router)
|
|
*
|
|
* @publicApi
|
|
*/
|
|
export interface UrlCreationOptions {
|
|
/**
|
|
* Specifies a root URI to use for relative navigation.
|
|
*
|
|
* For example, consider the following route configuration where the parent route
|
|
* has two children.
|
|
*
|
|
* ```
|
|
* [{
|
|
* path: 'parent',
|
|
* component: ParentComponent,
|
|
* children: [{
|
|
* path: 'list',
|
|
* component: ListComponent
|
|
* },{
|
|
* path: 'child',
|
|
* component: ChildComponent
|
|
* }]
|
|
* }]
|
|
* ```
|
|
*
|
|
* The following `go()` function navigates to the `list` route by
|
|
* interpreting the destination URI as relative to the activated `child` route
|
|
*
|
|
* ```
|
|
* @Component({...})
|
|
* class ChildComponent {
|
|
* constructor(private router: Router, private route: ActivatedRoute) {}
|
|
*
|
|
* go() {
|
|
* router.navigate(['../list'], { relativeTo: this.route });
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* A value of `null` or `undefined` indicates that the navigation commands should be applied
|
|
* relative to the root.
|
|
*/
|
|
relativeTo?: ActivatedRoute | null;
|
|
|
|
/**
|
|
* Sets query parameters to the URL.
|
|
*
|
|
* ```
|
|
* // Navigate to /results?page=1
|
|
* router.navigate(['/results'], { queryParams: { page: 1 } });
|
|
* ```
|
|
*/
|
|
queryParams?: Params | null;
|
|
|
|
/**
|
|
* Sets the hash fragment for the URL.
|
|
*
|
|
* ```
|
|
* // Navigate to /results#top
|
|
* router.navigate(['/results'], { fragment: 'top' });
|
|
* ```
|
|
*/
|
|
fragment?: string;
|
|
|
|
/**
|
|
* How to handle query parameters in the router link for the next navigation.
|
|
* One of:
|
|
* * `preserve` : Preserve current parameters.
|
|
* * `merge` : Merge new with current parameters.
|
|
*
|
|
* The "preserve" option discards any new query params:
|
|
* ```
|
|
* // from /view1?page=1 to/view2?page=1
|
|
* router.navigate(['/view2'], { queryParams: { page: 2 }, queryParamsHandling: "preserve"
|
|
* });
|
|
* ```
|
|
* The "merge" option appends new query params to the params from the current URL:
|
|
* ```
|
|
* // from /view1?page=1 to/view2?page=1&otherKey=2
|
|
* router.navigate(['/view2'], { queryParams: { otherKey: 2 }, queryParamsHandling: "merge"
|
|
* });
|
|
* ```
|
|
* In case of a key collision between current parameters and those in the `queryParams` object,
|
|
* the new value is used.
|
|
*
|
|
*/
|
|
queryParamsHandling?: QueryParamsHandling | null;
|
|
|
|
/**
|
|
* When true, preserves the URL fragment for the next navigation
|
|
*
|
|
* ```
|
|
* // Preserve fragment from /results#top to /view#top
|
|
* router.navigate(['/view'], { preserveFragment: true });
|
|
* ```
|
|
*/
|
|
preserveFragment?: boolean;
|
|
}
|
|
|
|
/**
|
|
* @description
|
|
*
|
|
* Options that modify the `Router` navigation strategy.
|
|
* Supply an object containing any of these properties to a `Router` navigation function to
|
|
* control how the target URL should be constructed or interpreted.
|
|
*
|
|
* @see {@link Router#navigate}
|
|
* @see {@link Router#navigateByUrl}
|
|
* @see {@link Router#createurltree}
|
|
* @see [Routing and Navigation guide](guide/router)
|
|
* @see {@link UrlCreationOptions}
|
|
* @see {@link NavigationBehaviorOptions}
|
|
*
|
|
* @publicApi
|
|
*/
|
|
export interface NavigationExtras extends UrlCreationOptions, NavigationBehaviorOptions {}
|
|
|
|
export type RestoredState = {
|
|
[k: string]: any;
|
|
// TODO(#27607): Remove `navigationId` and `ɵrouterPageId` and move to `ng` or `ɵ` namespace.
|
|
navigationId: number;
|
|
// The `ɵ` prefix is there to reduce the chance of colliding with any existing user properties on
|
|
// the history state.
|
|
ɵrouterPageId?: number;
|
|
};
|
|
|
|
/**
|
|
* Information about a navigation operation.
|
|
* Retrieve the most recent navigation object with the
|
|
* [Router.getCurrentNavigation() method](api/router/Router#getcurrentnavigation) .
|
|
*
|
|
* * *id* : The unique identifier of the current navigation.
|
|
* * *initialUrl* : The target URL passed into the `Router#navigateByUrl()` call before navigation.
|
|
* This is the value before the router has parsed or applied redirects to it.
|
|
* * *extractedUrl* : The initial target URL after being parsed with `UrlSerializer.extract()`.
|
|
* * *finalUrl* : The extracted URL after redirects have been applied.
|
|
* This URL may not be available immediately, therefore this property can be `undefined`.
|
|
* It is guaranteed to be set after the `RoutesRecognized` event fires.
|
|
* * *trigger* : Identifies how this navigation was triggered.
|
|
* -- 'imperative'--Triggered by `router.navigateByUrl` or `router.navigate`.
|
|
* -- 'popstate'--Triggered by a popstate event.
|
|
* -- 'hashchange'--Triggered by a hashchange event.
|
|
* * *extras* : A `NavigationExtras` options object that controlled the strategy used for this
|
|
* navigation.
|
|
* * *previousNavigation* : The previously successful `Navigation` object. Only one previous
|
|
* navigation is available, therefore this previous `Navigation` object has a `null` value for its
|
|
* own `previousNavigation`.
|
|
*
|
|
* @publicApi
|
|
*/
|
|
export interface Navigation {
|
|
/**
|
|
* The unique identifier of the current navigation.
|
|
*/
|
|
id: number;
|
|
/**
|
|
* The target URL passed into the `Router#navigateByUrl()` call before navigation. This is
|
|
* the value before the router has parsed or applied redirects to it.
|
|
*/
|
|
initialUrl: UrlTree;
|
|
/**
|
|
* The initial target URL after being parsed with `UrlHandlingStrategy.extract()`.
|
|
*/
|
|
extractedUrl: UrlTree;
|
|
/**
|
|
* The extracted URL after redirects have been applied.
|
|
* This URL may not be available immediately, therefore this property can be `undefined`.
|
|
* It is guaranteed to be set after the `RoutesRecognized` event fires.
|
|
*/
|
|
finalUrl?: UrlTree;
|
|
/**
|
|
* TODO(atscott): If we want to make StateManager public, they will need access to this. Note that
|
|
* it's already eventually exposed through router.routerState.
|
|
* @internal
|
|
*/
|
|
targetRouterState?: RouterState;
|
|
/**
|
|
* Identifies how this navigation was triggered.
|
|
*
|
|
* * 'imperative'--Triggered by `router.navigateByUrl` or `router.navigate`.
|
|
* * 'popstate'--Triggered by a popstate event.
|
|
* * 'hashchange'--Triggered by a hashchange event.
|
|
*/
|
|
trigger: 'imperative' | 'popstate' | 'hashchange';
|
|
/**
|
|
* Options that controlled the strategy used for this navigation.
|
|
* See `NavigationExtras`.
|
|
*/
|
|
extras: NavigationExtras;
|
|
/**
|
|
* The previously successful `Navigation` object. Only one previous navigation
|
|
* is available, therefore this previous `Navigation` object has a `null` value
|
|
* for its own `previousNavigation`.
|
|
*/
|
|
previousNavigation: Navigation | null;
|
|
}
|
|
|
|
export interface NavigationTransition {
|
|
id: number;
|
|
currentUrlTree: UrlTree;
|
|
extractedUrl: UrlTree;
|
|
currentRawUrl: UrlTree;
|
|
urlAfterRedirects?: UrlTree;
|
|
rawUrl: UrlTree;
|
|
extras: NavigationExtras;
|
|
resolve: any;
|
|
reject: any;
|
|
promise: Promise<boolean>;
|
|
source: NavigationTrigger;
|
|
restoredState: RestoredState | null;
|
|
currentSnapshot: RouterStateSnapshot;
|
|
targetSnapshot: RouterStateSnapshot | null;
|
|
currentRouterState: RouterState;
|
|
targetRouterState: RouterState | null;
|
|
guards: Checks;
|
|
guardsResult: GuardResult | null;
|
|
}
|
|
|
|
/**
|
|
* The interface from the Router needed by the transitions. Used to avoid a circular dependency on
|
|
* Router. This interface should be whittled down with future refactors. For example, we do not need
|
|
* to get `UrlSerializer` from the Router. We can instead inject it in `NavigationTransitions`
|
|
* directly.
|
|
*/
|
|
interface InternalRouterInterface {
|
|
config: Routes;
|
|
// All of these are public API of router interface and can change during runtime because they are
|
|
// writeable. Ideally, these would be removed and the values retrieved instead from the values
|
|
// available in DI.
|
|
errorHandler: (error: any) => any;
|
|
navigated: boolean;
|
|
routeReuseStrategy: RouteReuseStrategy;
|
|
onSameUrlNavigation: 'reload' | 'ignore';
|
|
}
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
export class NavigationTransitions {
|
|
currentNavigation: Navigation | null = null;
|
|
currentTransition: NavigationTransition | null = null;
|
|
lastSuccessfulNavigation: Navigation | null = null;
|
|
/**
|
|
* These events are used to communicate back to the Router about the state of the transition. The
|
|
* Router wants to respond to these events in various ways. Because the `NavigationTransition`
|
|
* class is not public, this event subject is not publicly exposed.
|
|
*/
|
|
readonly events = new Subject<Event | BeforeActivateRoutes | RedirectRequest>();
|
|
/**
|
|
* Used to abort the current transition with an error.
|
|
*/
|
|
readonly transitionAbortSubject = new Subject<Error>();
|
|
private readonly configLoader = inject(RouterConfigLoader);
|
|
private readonly environmentInjector = inject(EnvironmentInjector);
|
|
private readonly urlSerializer = inject(UrlSerializer);
|
|
private readonly rootContexts = inject(ChildrenOutletContexts);
|
|
private readonly location = inject(Location);
|
|
private readonly inputBindingEnabled = inject(INPUT_BINDER, {optional: true}) !== null;
|
|
private readonly titleStrategy?: TitleStrategy = inject(TitleStrategy);
|
|
private readonly options = inject(ROUTER_CONFIGURATION, {optional: true}) || {};
|
|
private readonly paramsInheritanceStrategy =
|
|
this.options.paramsInheritanceStrategy || 'emptyOnly';
|
|
private readonly urlHandlingStrategy = inject(UrlHandlingStrategy);
|
|
private readonly createViewTransition = inject(CREATE_VIEW_TRANSITION, {optional: true});
|
|
|
|
navigationId = 0;
|
|
get hasRequestedNavigation() {
|
|
return this.navigationId !== 0;
|
|
}
|
|
private transitions?: BehaviorSubject<NavigationTransition>;
|
|
/**
|
|
* Hook that enables you to pause navigation after the preactivation phase.
|
|
* Used by `RouterModule`.
|
|
*
|
|
* @internal
|
|
*/
|
|
afterPreactivation: () => Observable<void> = () => of(void 0);
|
|
/** @internal */
|
|
rootComponentType: Type<any> | null = null;
|
|
|
|
constructor() {
|
|
const onLoadStart = (r: Route) => this.events.next(new RouteConfigLoadStart(r));
|
|
const onLoadEnd = (r: Route) => this.events.next(new RouteConfigLoadEnd(r));
|
|
this.configLoader.onLoadEndListener = onLoadEnd;
|
|
this.configLoader.onLoadStartListener = onLoadStart;
|
|
}
|
|
|
|
complete() {
|
|
this.transitions?.complete();
|
|
}
|
|
|
|
handleNavigationRequest(
|
|
request: Pick<
|
|
NavigationTransition,
|
|
| 'source'
|
|
| 'restoredState'
|
|
| 'currentUrlTree'
|
|
| 'currentRawUrl'
|
|
| 'rawUrl'
|
|
| 'extras'
|
|
| 'resolve'
|
|
| 'reject'
|
|
| 'promise'
|
|
| 'currentSnapshot'
|
|
| 'currentRouterState'
|
|
>,
|
|
) {
|
|
const id = ++this.navigationId;
|
|
this.transitions?.next({...this.transitions.value, ...request, id});
|
|
}
|
|
|
|
setupNavigations(
|
|
router: InternalRouterInterface,
|
|
initialUrlTree: UrlTree,
|
|
initialRouterState: RouterState,
|
|
): Observable<NavigationTransition> {
|
|
this.transitions = new BehaviorSubject<NavigationTransition>({
|
|
id: 0,
|
|
currentUrlTree: initialUrlTree,
|
|
currentRawUrl: initialUrlTree,
|
|
extractedUrl: this.urlHandlingStrategy.extract(initialUrlTree),
|
|
urlAfterRedirects: this.urlHandlingStrategy.extract(initialUrlTree),
|
|
rawUrl: initialUrlTree,
|
|
extras: {},
|
|
resolve: null,
|
|
reject: null,
|
|
promise: Promise.resolve(true),
|
|
source: IMPERATIVE_NAVIGATION,
|
|
restoredState: null,
|
|
currentSnapshot: initialRouterState.snapshot,
|
|
targetSnapshot: null,
|
|
currentRouterState: initialRouterState,
|
|
targetRouterState: null,
|
|
guards: {canActivateChecks: [], canDeactivateChecks: []},
|
|
guardsResult: null,
|
|
});
|
|
return this.transitions.pipe(
|
|
filter((t) => t.id !== 0),
|
|
|
|
// Extract URL
|
|
map(
|
|
(t) =>
|
|
({
|
|
...t,
|
|
extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl),
|
|
}) as NavigationTransition,
|
|
),
|
|
|
|
// Using switchMap so we cancel executing navigations when a new one comes in
|
|
switchMap((overallTransitionState) => {
|
|
let completed = false;
|
|
let errored = false;
|
|
return of(overallTransitionState).pipe(
|
|
switchMap((t) => {
|
|
// It is possible that `switchMap` fails to cancel previous navigations if a new one happens synchronously while the operator
|
|
// is processing the `next` notification of that previous navigation. This can happen when a new navigation (say 2) cancels a
|
|
// previous one (1) and yet another navigation (3) happens synchronously in response to the `NavigationCancel` event for (1).
|
|
// https://github.com/ReactiveX/rxjs/issues/7455
|
|
if (this.navigationId > overallTransitionState.id) {
|
|
const cancellationReason =
|
|
typeof ngDevMode === 'undefined' || ngDevMode
|
|
? `Navigation ID ${overallTransitionState.id} is not equal to the current navigation id ${this.navigationId}`
|
|
: '';
|
|
this.cancelNavigationTransition(
|
|
overallTransitionState,
|
|
cancellationReason,
|
|
NavigationCancellationCode.SupersededByNewNavigation,
|
|
);
|
|
return EMPTY;
|
|
}
|
|
this.currentTransition = overallTransitionState;
|
|
// Store the Navigation object
|
|
this.currentNavigation = {
|
|
id: t.id,
|
|
initialUrl: t.rawUrl,
|
|
extractedUrl: t.extractedUrl,
|
|
trigger: t.source,
|
|
extras: t.extras,
|
|
previousNavigation: !this.lastSuccessfulNavigation
|
|
? null
|
|
: {
|
|
...this.lastSuccessfulNavigation,
|
|
previousNavigation: null,
|
|
},
|
|
};
|
|
const urlTransition =
|
|
!router.navigated || this.isUpdatingInternalState() || this.isUpdatedBrowserUrl();
|
|
|
|
const onSameUrlNavigation = t.extras.onSameUrlNavigation ?? router.onSameUrlNavigation;
|
|
if (!urlTransition && onSameUrlNavigation !== 'reload') {
|
|
const reason =
|
|
typeof ngDevMode === 'undefined' || ngDevMode
|
|
? `Navigation to ${t.rawUrl} was ignored because it is the same as the current Router URL.`
|
|
: '';
|
|
this.events.next(
|
|
new NavigationSkipped(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.rawUrl),
|
|
reason,
|
|
NavigationSkippedCode.IgnoredSameUrlNavigation,
|
|
),
|
|
);
|
|
t.resolve(null);
|
|
return EMPTY;
|
|
}
|
|
|
|
if (this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl)) {
|
|
return of(t).pipe(
|
|
// Fire NavigationStart event
|
|
switchMap((t) => {
|
|
const transition = this.transitions?.getValue();
|
|
this.events.next(
|
|
new NavigationStart(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
t.source,
|
|
t.restoredState,
|
|
),
|
|
);
|
|
if (transition !== this.transitions?.getValue()) {
|
|
return EMPTY;
|
|
}
|
|
|
|
// This delay is required to match old behavior that forced
|
|
// navigation to always be async
|
|
return Promise.resolve(t);
|
|
}),
|
|
|
|
// Recognize
|
|
recognize(
|
|
this.environmentInjector,
|
|
this.configLoader,
|
|
this.rootComponentType,
|
|
router.config,
|
|
this.urlSerializer,
|
|
this.paramsInheritanceStrategy,
|
|
),
|
|
|
|
// Update URL if in `eager` update mode
|
|
tap((t) => {
|
|
overallTransitionState.targetSnapshot = t.targetSnapshot;
|
|
overallTransitionState.urlAfterRedirects = t.urlAfterRedirects;
|
|
this.currentNavigation = {
|
|
...this.currentNavigation!,
|
|
finalUrl: t.urlAfterRedirects,
|
|
};
|
|
|
|
// Fire RoutesRecognized
|
|
const routesRecognized = new RoutesRecognized(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
this.urlSerializer.serialize(t.urlAfterRedirects!),
|
|
t.targetSnapshot!,
|
|
);
|
|
this.events.next(routesRecognized);
|
|
}),
|
|
);
|
|
} else if (
|
|
urlTransition &&
|
|
this.urlHandlingStrategy.shouldProcessUrl(t.currentRawUrl)
|
|
) {
|
|
/* When the current URL shouldn't be processed, but the previous one
|
|
* was, we handle this "error condition" by navigating to the
|
|
* previously successful URL, but leaving the URL intact.*/
|
|
const {id, extractedUrl, source, restoredState, extras} = t;
|
|
const navStart = new NavigationStart(
|
|
id,
|
|
this.urlSerializer.serialize(extractedUrl),
|
|
source,
|
|
restoredState,
|
|
);
|
|
this.events.next(navStart);
|
|
const targetSnapshot = createEmptyState(this.rootComponentType).snapshot;
|
|
|
|
this.currentTransition = overallTransitionState = {
|
|
...t,
|
|
targetSnapshot,
|
|
urlAfterRedirects: extractedUrl,
|
|
extras: {...extras, skipLocationChange: false, replaceUrl: false},
|
|
};
|
|
this.currentNavigation!.finalUrl = extractedUrl;
|
|
return of(overallTransitionState);
|
|
} else {
|
|
/* When neither the current or previous URL can be processed, do
|
|
* nothing other than update router's internal reference to the
|
|
* current "settled" URL. This way the next navigation will be coming
|
|
* from the current URL in the browser.
|
|
*/
|
|
const reason =
|
|
typeof ngDevMode === 'undefined' || ngDevMode
|
|
? `Navigation was ignored because the UrlHandlingStrategy` +
|
|
` indicated neither the current URL ${t.currentRawUrl} nor target URL ${t.rawUrl} should be processed.`
|
|
: '';
|
|
this.events.next(
|
|
new NavigationSkipped(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
reason,
|
|
NavigationSkippedCode.IgnoredByUrlHandlingStrategy,
|
|
),
|
|
);
|
|
t.resolve(null);
|
|
return EMPTY;
|
|
}
|
|
}),
|
|
|
|
// --- GUARDS ---
|
|
tap((t) => {
|
|
const guardsStart = new GuardsCheckStart(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
this.urlSerializer.serialize(t.urlAfterRedirects!),
|
|
t.targetSnapshot!,
|
|
);
|
|
this.events.next(guardsStart);
|
|
}),
|
|
|
|
map((t) => {
|
|
this.currentTransition = overallTransitionState = {
|
|
...t,
|
|
guards: getAllRouteGuards(t.targetSnapshot!, t.currentSnapshot, this.rootContexts),
|
|
};
|
|
return overallTransitionState;
|
|
}),
|
|
|
|
checkGuards(this.environmentInjector, (evt: Event) => this.events.next(evt)),
|
|
tap((t) => {
|
|
overallTransitionState.guardsResult = t.guardsResult;
|
|
if (t.guardsResult && typeof t.guardsResult !== 'boolean') {
|
|
throw redirectingNavigationError(this.urlSerializer, t.guardsResult);
|
|
}
|
|
|
|
const guardsEnd = new GuardsCheckEnd(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
this.urlSerializer.serialize(t.urlAfterRedirects!),
|
|
t.targetSnapshot!,
|
|
!!t.guardsResult,
|
|
);
|
|
this.events.next(guardsEnd);
|
|
}),
|
|
|
|
filter((t) => {
|
|
if (!t.guardsResult) {
|
|
this.cancelNavigationTransition(t, '', NavigationCancellationCode.GuardRejected);
|
|
return false;
|
|
}
|
|
return true;
|
|
}),
|
|
|
|
// --- RESOLVE ---
|
|
switchTap((t) => {
|
|
if (t.guards.canActivateChecks.length) {
|
|
return of(t).pipe(
|
|
tap((t) => {
|
|
const resolveStart = new ResolveStart(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
this.urlSerializer.serialize(t.urlAfterRedirects!),
|
|
t.targetSnapshot!,
|
|
);
|
|
this.events.next(resolveStart);
|
|
}),
|
|
switchMap((t) => {
|
|
let dataResolved = false;
|
|
return of(t).pipe(
|
|
resolveData(this.paramsInheritanceStrategy, this.environmentInjector),
|
|
tap({
|
|
next: () => (dataResolved = true),
|
|
complete: () => {
|
|
if (!dataResolved) {
|
|
this.cancelNavigationTransition(
|
|
t,
|
|
typeof ngDevMode === 'undefined' || ngDevMode
|
|
? `At least one route resolver didn't emit any value.`
|
|
: '',
|
|
NavigationCancellationCode.NoDataFromResolver,
|
|
);
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}),
|
|
tap((t) => {
|
|
const resolveEnd = new ResolveEnd(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
this.urlSerializer.serialize(t.urlAfterRedirects!),
|
|
t.targetSnapshot!,
|
|
);
|
|
this.events.next(resolveEnd);
|
|
}),
|
|
);
|
|
}
|
|
return undefined;
|
|
}),
|
|
|
|
// --- LOAD COMPONENTS ---
|
|
switchTap((t: NavigationTransition) => {
|
|
const loadComponents = (route: ActivatedRouteSnapshot): Array<Observable<void>> => {
|
|
const loaders: Array<Observable<void>> = [];
|
|
if (route.routeConfig?.loadComponent && !route.routeConfig._loadedComponent) {
|
|
loaders.push(
|
|
this.configLoader.loadComponent(route.routeConfig).pipe(
|
|
tap((loadedComponent) => {
|
|
route.component = loadedComponent;
|
|
}),
|
|
map(() => void 0),
|
|
),
|
|
);
|
|
}
|
|
for (const child of route.children) {
|
|
loaders.push(...loadComponents(child));
|
|
}
|
|
return loaders;
|
|
};
|
|
return combineLatest(loadComponents(t.targetSnapshot!.root)).pipe(
|
|
defaultIfEmpty(null),
|
|
take(1),
|
|
);
|
|
}),
|
|
|
|
switchTap(() => this.afterPreactivation()),
|
|
|
|
switchMap(() => {
|
|
const {currentSnapshot, targetSnapshot} = overallTransitionState;
|
|
const viewTransitionStarted = this.createViewTransition?.(
|
|
this.environmentInjector,
|
|
currentSnapshot.root,
|
|
targetSnapshot!.root,
|
|
);
|
|
|
|
// If view transitions are enabled, block the navigation until the view
|
|
// transition callback starts. Otherwise, continue immediately.
|
|
return viewTransitionStarted
|
|
? from(viewTransitionStarted).pipe(map(() => overallTransitionState))
|
|
: of(overallTransitionState);
|
|
}),
|
|
|
|
map((t: NavigationTransition) => {
|
|
const targetRouterState = createRouterState(
|
|
router.routeReuseStrategy,
|
|
t.targetSnapshot!,
|
|
t.currentRouterState,
|
|
);
|
|
this.currentTransition = overallTransitionState = {...t, targetRouterState};
|
|
this.currentNavigation!.targetRouterState = targetRouterState;
|
|
return overallTransitionState;
|
|
}),
|
|
|
|
tap(() => {
|
|
this.events.next(new BeforeActivateRoutes());
|
|
}),
|
|
|
|
activateRoutes(
|
|
this.rootContexts,
|
|
router.routeReuseStrategy,
|
|
(evt: Event) => this.events.next(evt),
|
|
this.inputBindingEnabled,
|
|
),
|
|
|
|
// Ensure that if some observable used to drive the transition doesn't
|
|
// complete, the navigation still finalizes This should never happen, but
|
|
// this is done as a safety measure to avoid surfacing this error (#49567).
|
|
take(1),
|
|
|
|
tap({
|
|
next: (t: NavigationTransition) => {
|
|
completed = true;
|
|
this.lastSuccessfulNavigation = this.currentNavigation;
|
|
this.events.next(
|
|
new NavigationEnd(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
this.urlSerializer.serialize(t.urlAfterRedirects!),
|
|
),
|
|
);
|
|
this.titleStrategy?.updateTitle(t.targetRouterState!.snapshot);
|
|
t.resolve(true);
|
|
},
|
|
complete: () => {
|
|
completed = true;
|
|
},
|
|
}),
|
|
|
|
// There used to be a lot more logic happening directly within the
|
|
// transition Observable. Some of this logic has been refactored out to
|
|
// other places but there may still be errors that happen there. This gives
|
|
// us a way to cancel the transition from the outside. This may also be
|
|
// required in the future to support something like the abort signal of the
|
|
// Navigation API where the navigation gets aborted from outside the
|
|
// transition.
|
|
takeUntil(
|
|
this.transitionAbortSubject.pipe(
|
|
tap((err) => {
|
|
throw err;
|
|
}),
|
|
),
|
|
),
|
|
|
|
finalize(() => {
|
|
/* When the navigation stream finishes either through error or success,
|
|
* we set the `completed` or `errored` flag. However, there are some
|
|
* situations where we could get here without either of those being set.
|
|
* For instance, a redirect during NavigationStart. Therefore, this is a
|
|
* catch-all to make sure the NavigationCancel event is fired when a
|
|
* navigation gets cancelled but not caught by other means. */
|
|
if (!completed && !errored) {
|
|
const cancelationReason =
|
|
typeof ngDevMode === 'undefined' || ngDevMode
|
|
? `Navigation ID ${overallTransitionState.id} is not equal to the current navigation id ${this.navigationId}`
|
|
: '';
|
|
this.cancelNavigationTransition(
|
|
overallTransitionState,
|
|
cancelationReason,
|
|
NavigationCancellationCode.SupersededByNewNavigation,
|
|
);
|
|
}
|
|
// Only clear current navigation if it is still set to the one that
|
|
// finalized.
|
|
if (this.currentTransition?.id === overallTransitionState.id) {
|
|
this.currentNavigation = null;
|
|
this.currentTransition = null;
|
|
}
|
|
}),
|
|
catchError((e) => {
|
|
errored = true;
|
|
/* This error type is issued during Redirect, and is handled as a
|
|
* cancellation rather than an error. */
|
|
if (isNavigationCancelingError(e)) {
|
|
this.events.next(
|
|
new NavigationCancel(
|
|
overallTransitionState.id,
|
|
this.urlSerializer.serialize(overallTransitionState.extractedUrl),
|
|
e.message,
|
|
e.cancellationCode,
|
|
),
|
|
);
|
|
|
|
// When redirecting, we need to delay resolving the navigation
|
|
// promise and push it to the redirect navigation
|
|
if (!isRedirectingNavigationCancelingError(e)) {
|
|
overallTransitionState.resolve(false);
|
|
} else {
|
|
this.events.next(new RedirectRequest(e.url, e.navigationBehaviorOptions));
|
|
}
|
|
|
|
/* All other errors should reset to the router's internal URL reference
|
|
* to the pre-error state. */
|
|
} else {
|
|
this.events.next(
|
|
new NavigationError(
|
|
overallTransitionState.id,
|
|
this.urlSerializer.serialize(overallTransitionState.extractedUrl),
|
|
e,
|
|
overallTransitionState.targetSnapshot ?? undefined,
|
|
),
|
|
);
|
|
try {
|
|
overallTransitionState.resolve(router.errorHandler(e));
|
|
} catch (ee) {
|
|
// TODO(atscott): consider flipping the default behavior of
|
|
// resolveNavigationPromiseOnError to be `resolve(false)` when
|
|
// undefined. This is the most sane thing to do given that
|
|
// applications very rarely handle the promise rejection and, as a
|
|
// result, would get "unhandled promise rejection" console logs.
|
|
// The vast majority of applications would not be affected by this
|
|
// change so omitting a migration seems reasonable. Instead,
|
|
// applications that rely on rejection can specifically opt-in to the
|
|
// old behavior.
|
|
if (this.options.resolveNavigationPromiseOnError) {
|
|
overallTransitionState.resolve(false);
|
|
} else {
|
|
overallTransitionState.reject(ee);
|
|
}
|
|
}
|
|
}
|
|
return EMPTY;
|
|
}),
|
|
);
|
|
// casting because `pipe` returns observable({}) when called with 8+ arguments
|
|
}),
|
|
) as Observable<NavigationTransition>;
|
|
}
|
|
|
|
private cancelNavigationTransition(
|
|
t: NavigationTransition,
|
|
reason: string,
|
|
code: NavigationCancellationCode,
|
|
) {
|
|
const navCancel = new NavigationCancel(
|
|
t.id,
|
|
this.urlSerializer.serialize(t.extractedUrl),
|
|
reason,
|
|
code,
|
|
);
|
|
this.events.next(navCancel);
|
|
t.resolve(false);
|
|
}
|
|
|
|
/**
|
|
* @returns Whether we're navigating to somewhere that is not what the Router is
|
|
* currently set to.
|
|
*/
|
|
private isUpdatingInternalState() {
|
|
// TODO(atscott): The serializer should likely be used instead of
|
|
// `UrlTree.toString()`. Custom serializers are often written to handle
|
|
// things better than the default one (objects, for example will be
|
|
// [Object object] with the custom serializer and be "the same" when they
|
|
// aren't).
|
|
// (Same for isUpdatedBrowserUrl)
|
|
return (
|
|
this.currentTransition?.extractedUrl.toString() !==
|
|
this.currentTransition?.currentUrlTree.toString()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @returns Whether we're updating the browser URL to something new (navigation is going
|
|
* to somewhere not displayed in the URL bar and we will update the URL
|
|
* bar if navigation succeeds).
|
|
*/
|
|
private isUpdatedBrowserUrl() {
|
|
// The extracted URL is the part of the URL that this application cares about. `extract` may
|
|
// return only part of the browser URL and that part may have not changed even if some other
|
|
// portion of the URL did.
|
|
const extractedBrowserUrl = this.urlHandlingStrategy.extract(
|
|
this.urlSerializer.parse(this.location.path(true)),
|
|
);
|
|
return (
|
|
extractedBrowserUrl.toString() !== this.currentTransition?.extractedUrl.toString() &&
|
|
!this.currentTransition?.extras.skipLocationChange
|
|
);
|
|
}
|
|
}
|
|
|
|
export function isBrowserTriggeredNavigation(source: NavigationTrigger) {
|
|
return source !== IMPERATIVE_NAVIGATION;
|
|
}
|