/**
* @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 {Compiler, inject, Injectable, Injector, NgZone, Type, ɵConsole as Console, ɵRuntimeError as RuntimeError} from '@angular/core';
import {BehaviorSubject, Observable, of, SubscriptionLike} from 'rxjs';
import {createUrlTree} from './create_url_tree';
import {RuntimeErrorCode} from './errors';
import {Event, NavigationTrigger} from './events';
import {NavigationBehaviorOptions, Routes} from './models';
import {Navigation, NavigationExtras, NavigationTransition, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
import {TitleStrategy} from './page_title_strategy';
import {RouteReuseStrategy} from './route_reuse_strategy';
import {ErrorHandler, ExtraOptions, ROUTER_CONFIGURATION} from './router_config';
import {ROUTES} from './router_config_loader';
import {ChildrenOutletContexts} from './router_outlet_context';
import {createEmptyState, RouterState} from './router_state';
import {Params} from './shared';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {containsTree, IsActiveMatchOptions, isUrlTree, UrlSerializer, UrlTree} from './url_tree';
import {flatten} from './utils/collection';
import {standardizeConfig, validateConfig} from './utils/config';
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
function defaultErrorHandler(error: any): any {
throw error;
}
function defaultMalformedUriErrorHandler(
error: URIError, urlSerializer: UrlSerializer, url: string): UrlTree {
return urlSerializer.parse('/');
}
/**
* The equivalent `IsActiveMatchOptions` options for `Router.isActive` is called with `true`
* (exact = true).
*/
export const exactMatchOptions: IsActiveMatchOptions = {
paths: 'exact',
fragment: 'ignored',
matrixParams: 'ignored',
queryParams: 'exact'
};
/**
* The equivalent `IsActiveMatchOptions` options for `Router.isActive` is called with `false`
* (exact = false).
*/
export const subsetMatchOptions: IsActiveMatchOptions = {
paths: 'subset',
fragment: 'ignored',
matrixParams: 'ignored',
queryParams: 'subset'
};
/**
* @description
*
* A service that provides navigation among views and URL manipulation capabilities.
*
* @see `Route`.
* @see [Routing and Navigation Guide](guide/router).
*
* @ngModule RouterModule
*
* @publicApi
*/
@Injectable({providedIn: 'root'})
export class Router {
/**
* Represents the activated `UrlTree` that the `Router` is configured to handle (through
* `UrlHandlingStrategy`). That is, after we find the route config tree that we're going to
* activate, run guards, and are just about to activate the route, we set the currentUrlTree.
*
* This should match the `browserUrlTree` when a navigation succeeds. If the
* `UrlHandlingStrategy.shouldProcessUrl` is `false`, only the `browserUrlTree` is updated.
* @internal
*/
currentUrlTree: UrlTree;
/**
* Meant to represent the entire browser url after a successful navigation. In the life of a
* navigation transition:
* 1. The rawUrl represents the full URL that's being navigated to
* 2. We apply redirects, which might only apply to _part_ of the URL (due to
* `UrlHandlingStrategy`).
* 3. Right before activation (because we assume activation will succeed), we update the
* rawUrlTree to be a combination of the urlAfterRedirects (again, this might only apply to part
* of the initial url) and the rawUrl of the transition (which was the original navigation url in
* its full form).
* @internal
*
* Note that this is _only_ here to support `UrlHandlingStrategy.extract` and
* `UrlHandlingStrategy.shouldProcessUrl`. If those didn't exist, we could get by with
* `currentUrlTree` alone. If a new Router were to be provided (i.e. one that works with the
* browser navigation API), we should think about whether this complexity should be carried over.
*
* - extract: `rawUrlTree` is needed because `extract` may only return part
* of the navigation URL. Thus, `currentUrlTree` may only represent _part_ of the browser URL.
* When a navigation gets cancelled and we need to reset the URL or a new navigation occurs, we
* need to know the _whole_ browser URL, not just the part handled by UrlHandlingStrategy.
* - shouldProcessUrl: When this returns `false`, the router just ignores the navigation but still
* updates the `rawUrlTree` with the assumption that the navigation was caused by the location
* change listener due to a URL update by the AngularJS router. In this case, we still need to
* know what the browser's URL is for future navigations.
*
*/
rawUrlTree: UrlTree;
/**
* Meant to represent the part of the browser url that the `Router` is set up to handle (via the
* `UrlHandlingStrategy`). This value is updated immediately after the browser url is updated (or
* the browser url update is skipped via `skipLocationChange`). With that, note that
* `browserUrlTree` _may not_ reflect the actual browser URL for two reasons:
*
* 1. `UrlHandlingStrategy` only handles part of the URL
* 2. `skipLocationChange` does not update the browser url.
*
* So to reiterate, `browserUrlTree` only represents the Router's internal understanding of the
* current route, either before guards with `urlUpdateStrategy === 'eager'` or right before
* activation with `'deferred'`.
*
* This should match the `currentUrlTree` when the navigation succeeds.
* @internal
*/
browserUrlTree: UrlTree;
private disposed = false;
private locationSubscription?: SubscriptionLike;
// TODO(b/260747083): This should not exist and navigationId should be private in
// `NavigationTransitions`
private get navigationId() {
return this.navigationTransitions.navigationId;
}
/**
* The id of the currently active page in the router.
* Updated to the transition's target id on a successful navigation.
*
* This is used to track what page the router last activated. When an attempted navigation fails,
* the router can then use this to compute how to restore the state back to the previously active
* page.
*/
private currentPageId: number = 0;
/**
* The ɵrouterPageId of whatever page is currently active in the browser history. This is
* important for computing the target page id for new navigations because we need to ensure each
* page id in the browser history is 1 more than the previous entry.
*/
private get browserPageId(): number|undefined {
return (this.location.getState() as RestoredState | null)?.ɵrouterPageId;
}
private console = inject(Console);
private isNgZoneEnabled: boolean = false;
/**
* An event stream for routing events.
*/
public get events(): Observable<Event> {
// TODO(atscott): This _should_ be events.asObservable(). However, this change requires internal
// cleanup: tests are doing `(route.events as Subject<Event>).next(...)`. This isn't
// allowed/supported but we still have to fix these or file bugs against the teams before making
// the change.
return this.navigationTransitions.events;
}
/**
* The current state of routing in this NgModule.
*/
public readonly routerState: RouterState;
private options = inject(ROUTER_CONFIGURATION, {optional: true}) || {};
/**
* A handler for navigation errors in this NgModule.
*/
errorHandler = this.options.errorHandler || defaultErrorHandler;
/**
* A handler for errors thrown by `Router.parseUrl(url)`
* when `url` contains an invalid character.
* The most common case is a `%` sign
* that's not encoded and is not part of a percent encoded sequence.
*/
malformedUriErrorHandler =
this.options.malformedUriErrorHandler || defaultMalformedUriErrorHandler;
/**
* True if at least one navigation event has occurred,
* false otherwise.
*/
navigated: boolean = false;
private lastSuccessfulId: number = -1;
/**
* Hook that enables you to pause navigation after the preactivation phase.
* Used by `RouterModule`.
*
* @internal
*/
afterPreactivation: () => Observable<void> = () => of(void 0);
/**
* A strategy for extracting and merging URLs.
* Used for AngularJS to Angular migrations.
*/
urlHandlingStrategy = inject(UrlHandlingStrategy);
/**
* A strategy for re-using routes.
*/
routeReuseStrategy = inject(RouteReuseStrategy);
/**
* A strategy for setting the title based on the `routerState`.
*/
titleStrategy?: TitleStrategy = inject(TitleStrategy);
/**
* How to handle a navigation request to the current URL. One of:
*
* - `'ignore'` : The router ignores the request.
* - `'reload'` : The router reloads the URL. Use to implement a "refresh" feature.
*
* Note that this only configures whether the Route reprocesses the URL and triggers related
* action and events like redirects, guards, and resolvers. By default, the router re-uses a
* component instance when it re-navigates to the same component type without visiting a different
* component first. This behavior is configured by the `RouteReuseStrategy`. In order to reload
* routed components on same url navigation, you need to set `onSameUrlNavigation` to `'reload'`
* _and_ provide a `RouteReuseStrategy` which returns `false` for `shouldReuseRoute`.
*/
onSameUrlNavigation: 'reload'|'ignore' = this.options.onSameUrlNavigation || 'ignore';
/**
* How to merge parameters, data, resolved data, and title from parent to child
* routes. One of:
*
* - `'emptyOnly'` : Inherit parent parameters, data, and resolved data
* for path-less or component-less routes.
* - `'always'` : Inherit parent parameters, data, and resolved data
* for all child routes.
*/
paramsInheritanceStrategy: 'emptyOnly'|'always' =
this.options.paramsInheritanceStrategy || 'emptyOnly';
/**
* Determines when the router updates the browser URL.
* By default (`"deferred"`), updates the browser URL after navigation has finished.
* Set to `'eager'` to update the browser URL at the beginning of navigation.
* You can choose to update early so that, if navigation fails,
* you can show an error message with the URL that failed.
*/
urlUpdateStrategy: 'deferred'|'eager' = this.options.urlUpdateStrategy || 'deferred';
/**
* Configures how the Router attempts to restore state when a navigation is cancelled.
*
* 'replace' - Always uses `location.replaceState` to set the browser state to the state of the
* router before the navigation started. This means that if the URL of the browser is updated
* _before_ the navigation is canceled, the Router will simply replace the item in history rather
* than trying to restore to the previous location in the session history. This happens most
* frequently with `urlUpdateStrategy: 'eager'` and navigations with the browser back/forward
* buttons.
*
* 'computed' - Will attempt to return to the same index in the session history that corresponds
* to the Angular route when the navigation gets cancelled. For example, if the browser back
* button is clicked and the navigation is cancelled, the Router will trigger a forward navigation
* and vice versa.
*
* Note: the 'computed' option is incompatible with any `UrlHandlingStrategy` which only
* handles a portion of the URL because the history restoration navigates to the previous place in
* the browser history rather than simply resetting a portion of the URL.
*
* The default value is `replace`.
*
*/
canceledNavigationResolution: 'replace'|'computed' =
this.options.canceledNavigationResolution || 'replace';
config: Routes = flatten(inject(ROUTES, {optional: true}) ?? []);
private readonly navigationTransitions = inject(NavigationTransitions);
private readonly urlSerializer = inject(UrlSerializer);
private readonly location = inject(Location);
/** @internal */
rootComponentType: Type<any>|null = null;
constructor() {
this.isNgZoneEnabled = inject(NgZone) instanceof NgZone && NgZone.isInAngularZone();
this.resetConfig(this.config);
this.currentUrlTree = new UrlTree();
this.rawUrlTree = this.currentUrlTree;
this.browserUrlTree = this.currentUrlTree;
this.routerState = createEmptyState(this.currentUrlTree, this.rootComponentType);
this.navigationTransitions.setupNavigations(this).subscribe(
t => {
this.lastSuccessfulId = t.id;
this.currentPageId = t.targetPageId;
},
e => {
this.console.warn(`Unhandled Navigation Error: ${e}`);
});
}
/** @internal */
resetRootComponentType(rootComponentType: Type<any>): void {
this.rootComponentType = rootComponentType;
// TODO: vsavkin router 4.0 should make the root component set to null
// this will simplify the lifecycle of the router.
this.routerState.root.component = this.rootComponentType;
}
/**
* Sets up the location change listener and performs the initial navigation.
*/
initialNavigation(): void {
this.setUpLocationChangeListener();
if (!this.navigationTransitions.hasRequestedNavigation) {
this.navigateByUrl(this.location.path(true), {replaceUrl: true});
}
}
/**
* Sets up the location change listener. This listener detects navigations triggered from outside
* the Router (the browser back/forward buttons, for example) and schedules a corresponding Router
* navigation so that the correct events, guards, etc. are triggered.
*/
setUpLocationChangeListener(): void {
// Don't need to use Zone.wrap any more, because zone.js
// already patch onPopState, so location change callback will
// run into ngZone
if (!this.locationSubscription) {
this.locationSubscription = this.location.subscribe(event => {
const source = event['type'] === 'popstate' ? 'popstate' : 'hashchange';
if (source === 'popstate') {
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
// hybrid apps.
setTimeout(() => {
const extras: NavigationExtras = {replaceUrl: true};
// TODO: restoredState should always include the entire state, regardless
// of navigationId. This requires a breaking change to update the type on
// NavigationStart’s restoredState, which currently requires navigationId
// to always be present. The Router used to only restore history state if
// a navigationId was present.
// The stored navigationId is used by the RouterScroller to retrieve the scroll
// position for the page.
const restoredState = event.state?.navigationId ? event.state : null;
// Separate to NavigationStart.restoredState, we must also restore the state to
// history.state and generate a new navigationId, since it will be overwritten
if (event.state) {
const stateCopy = {...event.state} as Partial<RestoredState>;
delete stateCopy.navigationId;
delete stateCopy.ɵrouterPageId;
if (Object.keys(stateCopy).length !== 0) {
extras.state = stateCopy;
}
}
const urlTree = this.parseUrl(event['url']!);
this.scheduleNavigation(urlTree, source, restoredState, extras);
}, 0);
}
});
}
}
/** The current URL. */
get url(): string {
return this.serializeUrl(this.currentUrlTree);
}
/**
* Returns the current `Navigation` object when the router is navigating,
* and `null` when idle.
*/
getCurrentNavigation(): Navigation|null {
return this.navigationTransitions.currentNavigation;
}
/**
* Resets the route configuration used for navigation and generating links.
*
* @param config The route array for the new configuration.
*
* @usageNotes
*
* ```
* router.resetConfig([
* { path: 'team/:id', component: TeamCmp, children: [
* { path: 'simple', component: SimpleCmp },
* { path: 'user/:name', component: UserCmp }
* ]}
* ]);
* ```
*/
resetConfig(config: Routes): void {
NG_DEV_MODE && validateConfig(config);
this.config = config.map(standardizeConfig);
this.navigated = false;
this.lastSuccessfulId = -1;
}
/** @nodoc */
ngOnDestroy(): void {
this.dispose();
}
/** Disposes of the router. */
dispose(): void {
this.navigationTransitions.complete();
if (this.locationSubscription) {
this.locationSubscription.unsubscribe();
this.locationSubscription = undefined;
}
this.disposed = true;
}
/**
* Appends URL segments to the current URL tree to create a new URL tree.
*
* @param commands An array of URL fragments with which to construct the new URL tree.
* If the path is static, can be the literal URL string. For a dynamic path, pass an array of path
* segments, followed by the parameters for each segment.
* The fragments are applied to the current URL tree or the one provided in the `relativeTo`
* property of the options object, if supplied.
* @param navigationExtras Options that control the navigation strategy.
* @returns The new URL tree.
*
* @usageNotes
*
* ```
* // create /team/33/user/11
* router.createUrlTree(['/team', 33, 'user', 11]);
*
* // create /team/33;expand=true/user/11
* router.createUrlTree(['/team', 33, {expand: true}, 'user', 11]);
*
* // you can collapse static segments like this (this works only with the first passed-in value):
* router.createUrlTree(['/team/33/user', userId]);
*
* // If the first segment can contain slashes, and you do not want the router to split it,
* // you can do the following:
* router.createUrlTree([{segmentPath: '/one/two'}]);
*
* // create /team/33/(user/11//right:chat)
* router.createUrlTree(['/team', 33, {outlets: {primary: 'user/11', right: 'chat'}}]);
*
* // remove the right secondary node
* router.createUrlTree(['/team', 33, {outlets: {primary: 'user/11', right: null}}]);
*
* // assuming the current url is `/team/33/user/11` and the route points to `user/11`
*
* // navigate to /team/33/user/11/details
* router.createUrlTree(['details'], {relativeTo: route});
*
* // navigate to /team/33/user/22
* router.createUrlTree(['../22'], {relativeTo: route});
*
* // navigate to /team/44/user/22
* router.createUrlTree(['../../team/44/user/22'], {relativeTo: route});
*
* Note that a value of `null` or `undefined` for `relativeTo` indicates that the
* tree should be created relative to the root.
* ```
*/
createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree {
const {relativeTo, queryParams, fragment, queryParamsHandling, preserveFragment} =
navigationExtras;
const a = relativeTo || this.routerState.root;
const f = preserveFragment ? this.currentUrlTree.fragment : fragment;
let q: Params|null = null;
switch (queryParamsHandling) {
case 'merge':
q = {...this.currentUrlTree.queryParams, ...queryParams};
break;
case 'preserve':
q = this.currentUrlTree.queryParams;
break;
default:
q = queryParams || null;
}
if (q !== null) {
q = this.removeEmptyProps(q);
}
return createUrlTree(a, this.currentUrlTree, commands, q, f ?? null);
}
/**
* Navigates to a view using an absolute route path.
*
* @param url An absolute path for a defined route. The function does not apply any delta to the
* current URL.
* @param extras An object containing properties that modify the navigation strategy.
*
* @returns A Promise that resolves to 'true' when navigation succeeds,
* to 'false' when navigation fails, or is rejected on error.
*
* @usageNotes
*
* The following calls request navigation to an absolute path.
*
* ```
* router.navigateByUrl("/team/33/user/11");
*
* // Navigate without updating the URL
* router.navigateByUrl("/team/33/user/11", { skipLocationChange: true });
* ```
*
* @see [Routing and Navigation guide](guide/router)
*
*/
navigateByUrl(url: string|UrlTree, extras: NavigationBehaviorOptions = {
skipLocationChange: false
}): Promise<boolean> {
if (typeof ngDevMode === 'undefined' ||
ngDevMode && this.isNgZoneEnabled && !NgZone.isInAngularZone()) {
this.console.warn(
`Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`);
}
const urlTree = isUrlTree(url) ? url : this.parseUrl(url);
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
}
/**
* Navigate based on the provided array of commands and a starting point.
* If no starting route is provided, the navigation is absolute.
*
* @param commands An array of URL fragments with which to construct the target URL.
* If the path is static, can be the literal URL string. For a dynamic path, pass an array of path
* segments, followed by the parameters for each segment.
* The fragments are applied to the current URL or the one provided in the `relativeTo` property
* of the options object, if supplied.
* @param extras An options object that determines how the URL should be constructed or
* interpreted.
*
* @returns A Promise that resolves to `true` when navigation succeeds, to `false` when navigation
* fails,
* or is rejected on error.
*
* @usageNotes
*
* The following calls request navigation to a dynamic route path relative to the current URL.
*
* ```
* router.navigate(['team', 33, 'user', 11], {relativeTo: route});
*
* // Navigate without updating the URL, overriding the default behavior
* router.navigate(['team', 33, 'user', 11], {relativeTo: route, skipLocationChange: true});
* ```
*
* @see [Routing and Navigation guide](guide/router)
*
*/
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> {
validateCommands(commands);
return this.navigateByUrl(this.createUrlTree(commands, extras), extras);
}
/** Serializes a `UrlTree` into a string */
serializeUrl(url: UrlTree): string {
return this.urlSerializer.serialize(url);
}
/** Parses a string into a `UrlTree` */
parseUrl(url: string): UrlTree {
let urlTree: UrlTree;
try {
urlTree = this.urlSerializer.parse(url);
} catch (e) {
urlTree = this.malformedUriErrorHandler(e as URIError, this.urlSerializer, url);
}
return urlTree;
}
/**
* Returns whether the url is activated.
*
* @deprecated
* Use `IsActiveMatchOptions` instead.
*
* - The equivalent `IsActiveMatchOptions` for `true` is
* `{paths: 'exact', queryParams: 'exact', fragment: 'ignored', matrixParams: 'ignored'}`.
* - The equivalent for `false` is
* `{paths: 'subset', queryParams: 'subset', fragment: 'ignored', matrixParams: 'ignored'}`.
*/
isActive(url: string|UrlTree, exact: boolean): boolean;
/**
* Returns whether the url is activated.
*/
isActive(url: string|UrlTree, matchOptions: IsActiveMatchOptions): boolean;
/** @internal */
isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean;
isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean {
let options: IsActiveMatchOptions;
if (matchOptions === true) {
options = {...exactMatchOptions};
} else if (matchOptions === false) {
options = {...subsetMatchOptions};
} else {
options = matchOptions;
}
if (isUrlTree(url)) {
return containsTree(this.currentUrlTree, url, options);
}
const urlTree = this.parseUrl(url);
return containsTree(this.currentUrlTree, urlTree, options);
}
private removeEmptyProps(params: Params): Params {
return Object.keys(params).reduce((result: Params, key: string) => {
const value: any = params[key];
if (value !== null && value !== undefined) {
result[key] = value;
}
return result;
}, {});
}
/** @internal */
scheduleNavigation(
rawUrl: UrlTree, source: NavigationTrigger, restoredState: RestoredState|null,
extras: NavigationExtras,
priorPromise?: {resolve: any, reject: any, promise: Promise<boolean>}): Promise<boolean> {
if (this.disposed) {
return Promise.resolve(false);
}
let resolve: any;
let reject: any;
let promise: Promise<boolean>;
if (priorPromise) {
resolve = priorPromise.resolve;
reject = priorPromise.reject;
promise = priorPromise.promise;
} else {
promise = new Promise<boolean>((res, rej) => {
resolve = res;
reject = rej;
});
}
let targetPageId: number;
if (this.canceledNavigationResolution === 'computed') {
const isInitialPage = this.currentPageId === 0;
if (isInitialPage) {
restoredState = this.location.getState() as RestoredState | null;
}
// If the `ɵrouterPageId` exist in the state then `targetpageId` should have the value of
// `ɵrouterPageId`. This is the case for something like a page refresh where we assign the
// target id to the previously set value for that page.
if (restoredState && restoredState.ɵrouterPageId) {
targetPageId = restoredState.ɵrouterPageId;
} else {
// If we're replacing the URL or doing a silent navigation, we do not want to increment the
// page id because we aren't pushing a new entry to history.
if (extras.replaceUrl || extras.skipLocationChange) {
targetPageId = this.browserPageId ?? 0;
} else {
targetPageId = (this.browserPageId ?? 0) + 1;
}
}
} else {
// This is unused when `canceledNavigationResolution` is not computed.
targetPageId = 0;
}
this.navigationTransitions.handleNavigationRequest({
targetPageId,
source,
restoredState,
currentUrlTree: this.currentUrlTree,
currentRawUrl: this.currentUrlTree,
rawUrl,
extras,
resolve,
reject,
promise,
currentSnapshot: this.routerState.snapshot,
currentRouterState: this.routerState
});
// Make sure that the error is propagated even though `processNavigations` catch
// handler does not rethrow
return promise.catch((e: any) => {
return Promise.reject(e);
});
}
/** @internal */
setBrowserUrl(url: UrlTree, transition: NavigationTransition) {
const path = this.urlSerializer.serialize(url);
const state = {
...transition.extras.state,
...this.generateNgRouterState(transition.id, transition.targetPageId)
};
if (this.location.isCurrentPathEqualTo(path) || !!transition.extras.replaceUrl) {
this.location.replaceState(path, '', state);
} else {
this.location.go(path, '', state);
}
}
/**
* Performs the necessary rollback action to restore the browser URL to the
* state before the transition.
* @internal
*/
restoreHistory(transition: NavigationTransition, restoringFromCaughtError = false) {
if (this.canceledNavigationResolution === 'computed') {
const targetPagePosition = this.currentPageId - transition.targetPageId;
// The navigator change the location before triggered the browser event,
// so we need to go back to the current url if the navigation is canceled.
// Also, when navigation gets cancelled while using url update strategy eager, then we need to
// go back. Because, when `urlUpdateStrategy` is `eager`; `setBrowserUrl` method is called
// before any verification.
const browserUrlUpdateOccurred =
(transition.source === 'popstate' || this.urlUpdateStrategy === 'eager' ||
this.currentUrlTree === this.getCurrentNavigation()?.finalUrl);
if (browserUrlUpdateOccurred && targetPagePosition !== 0) {
this.location.historyGo(targetPagePosition);
} else if (
this.currentUrlTree === this.getCurrentNavigation()?.finalUrl &&
targetPagePosition === 0) {
// We got to the activation stage (where currentUrlTree is set to the navigation's
// finalUrl), but we weren't moving anywhere in history (skipLocationChange or replaceUrl).
// We still need to reset the router state back to what it was when the navigation started.
this.resetState(transition);
// TODO(atscott): resetting the `browserUrlTree` should really be done in `resetState`.
// Investigate if this can be done by running TGP.
this.browserUrlTree = transition.currentUrlTree;
this.resetUrlToCurrentUrlTree();
} else {
// The browser URL and router state was not updated before the navigation cancelled so
// there's no restoration needed.
}
} else if (this.canceledNavigationResolution === 'replace') {
// TODO(atscott): It seems like we should _always_ reset the state here. It would be a no-op
// for `deferred` navigations that haven't change the internal state yet because guards
// reject. For 'eager' navigations, it seems like we also really should reset the state
// because the navigation was cancelled. Investigate if this can be done by running TGP.
if (restoringFromCaughtError) {
this.resetState(transition);
}
this.resetUrlToCurrentUrlTree();
}
}
private resetState(t: NavigationTransition): void {
(this as {routerState: RouterState}).routerState = t.currentRouterState;
this.currentUrlTree = t.currentUrlTree;
// Note here that we use the urlHandlingStrategy to get the reset `rawUrlTree` because it may be
// configured to handle only part of the navigation URL. This means we would only want to reset
// the part of the navigation handled by the Angular router rather than the whole URL. In
// addition, the URLHandlingStrategy may be configured to specifically preserve parts of the URL
// when merging, such as the query params so they are not lost on a refresh.
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl);
}
private resetUrlToCurrentUrlTree(): void {
this.location.replaceState(
this.urlSerializer.serialize(this.rawUrlTree), '',
this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId));
}
private generateNgRouterState(navigationId: number, routerPageId?: number) {
if (this.canceledNavigationResolution === 'computed') {
return {navigationId, ɵrouterPageId: routerPageId};
}
return {navigationId};
}
}
function validateCommands(commands: string[]): void {
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
if (cmd == null) {
throw new RuntimeError(
RuntimeErrorCode.NULLISH_COMMAND,
NG_DEV_MODE && `The requested path contains ${cmd} segment at index ${i}`);
}
}
}