@angular/router
Version:
Angular - the routing library
1,325 lines (1,319 loc) • 295 kB
JavaScript
/**
* @license Angular v15.1.5
* (c) 2010-2022 Google LLC. https://angular.io/
* License: MIT
*/
import * as i0 from '@angular/core';
import { ɵisObservable, ɵisPromise, ɵRuntimeError, Injectable, EventEmitter, inject, ViewContainerRef, ChangeDetectorRef, EnvironmentInjector, Directive, Input, Output, Component, createEnvironmentInjector, isStandalone, ComponentFactoryResolver, ɵisInjectable, InjectionToken, InjectFlags, NgModuleFactory, ɵConsole, NgZone, ɵcoerceToBoolean, ɵɵsanitizeUrlOrResourceUrl, Attribute, HostBinding, HostListener, Optional, ContentChildren, makeEnvironmentProviders, APP_BOOTSTRAP_LISTENER, ENVIRONMENT_INITIALIZER, Injector, ApplicationRef, APP_INITIALIZER, NgProbeToken, SkipSelf, NgModule, Inject, Version } from '@angular/core';
import { from, of, BehaviorSubject, EmptyError, combineLatest, concat, defer, pipe, throwError, Observable, EMPTY, ConnectableObservable, Subject } from 'rxjs';
import * as i3 from '@angular/common';
import { Location, ViewportScroller, LOCATION_INITIALIZED, LocationStrategy, HashLocationStrategy, PathLocationStrategy } from '@angular/common';
import { map, switchMap, take, startWith, filter, mergeMap, first, concatMap, tap, catchError, scan, last as last$1, takeWhile, defaultIfEmpty, takeLast, mapTo, finalize, refCount, mergeAll } from 'rxjs/operators';
import * as i1 from '@angular/platform-browser';
/**
* The primary routing outlet.
*
* @publicApi
*/
const PRIMARY_OUTLET = 'primary';
/**
* A private symbol used to store the value of `Route.title` inside the `Route.data` if it is a
* static string or `Route.resolve` if anything else. This allows us to reuse the existing route
* data/resolvers to support the title feature without new instrumentation in the `Router` pipeline.
*/
const RouteTitleKey = Symbol('RouteTitle');
class ParamsAsMap {
constructor(params) {
this.params = params || {};
}
has(name) {
return Object.prototype.hasOwnProperty.call(this.params, name);
}
get(name) {
if (this.has(name)) {
const v = this.params[name];
return Array.isArray(v) ? v[0] : v;
}
return null;
}
getAll(name) {
if (this.has(name)) {
const v = this.params[name];
return Array.isArray(v) ? v : [v];
}
return [];
}
get keys() {
return Object.keys(this.params);
}
}
/**
* Converts a `Params` instance to a `ParamMap`.
* @param params The instance to convert.
* @returns The new map instance.
*
* @publicApi
*/
function convertToParamMap(params) {
return new ParamsAsMap(params);
}
/**
* Matches the route configuration (`route`) against the actual URL (`segments`).
*
* When no matcher is defined on a `Route`, this is the matcher used by the Router by default.
*
* @param segments The remaining unmatched segments in the current navigation
* @param segmentGroup The current segment group being matched
* @param route The `Route` to match against.
*
* @see UrlMatchResult
* @see Route
*
* @returns The resulting match information or `null` if the `route` should not match.
* @publicApi
*/
function defaultUrlMatcher(segments, segmentGroup, route) {
const parts = route.path.split('/');
if (parts.length > segments.length) {
// The actual URL is shorter than the config, no match
return null;
}
if (route.pathMatch === 'full' &&
(segmentGroup.hasChildren() || parts.length < segments.length)) {
// The config is longer than the actual URL but we are looking for a full match, return null
return null;
}
const posParams = {};
// Check each config part against the actual URL
for (let index = 0; index < parts.length; index++) {
const part = parts[index];
const segment = segments[index];
const isParameter = part.startsWith(':');
if (isParameter) {
posParams[part.substring(1)] = segment;
}
else if (part !== segment.path) {
// The actual URL part does not match the config, no match
return null;
}
}
return { consumed: segments.slice(0, parts.length), posParams };
}
function shallowEqualArrays(a, b) {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; ++i) {
if (!shallowEqual(a[i], b[i]))
return false;
}
return true;
}
function shallowEqual(a, b) {
// While `undefined` should never be possible, it would sometimes be the case in IE 11
// and pre-chromium Edge. The check below accounts for this edge case.
const k1 = a ? Object.keys(a) : undefined;
const k2 = b ? Object.keys(b) : undefined;
if (!k1 || !k2 || k1.length != k2.length) {
return false;
}
let key;
for (let i = 0; i < k1.length; i++) {
key = k1[i];
if (!equalArraysOrString(a[key], b[key])) {
return false;
}
}
return true;
}
/**
* Test equality for arrays of strings or a string.
*/
function equalArraysOrString(a, b) {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length)
return false;
const aSorted = [...a].sort();
const bSorted = [...b].sort();
return aSorted.every((val, index) => bSorted[index] === val);
}
else {
return a === b;
}
}
/**
* Flattens single-level nested arrays.
*/
function flatten(arr) {
return Array.prototype.concat.apply([], arr);
}
/**
* Return the last element of an array.
*/
function last(a) {
return a.length > 0 ? a[a.length - 1] : null;
}
/**
* Verifys all booleans in an array are `true`.
*/
function and(bools) {
return !bools.some(v => !v);
}
function forEach(map, callback) {
for (const prop in map) {
if (map.hasOwnProperty(prop)) {
callback(map[prop], prop);
}
}
}
function wrapIntoObservable(value) {
if (ɵisObservable(value)) {
return value;
}
if (ɵisPromise(value)) {
// Use `Promise.resolve()` to wrap promise-like instances.
// Required ie when a Resolver returns a AngularJS `$q` promise to correctly trigger the
// change detection.
return from(Promise.resolve(value));
}
return of(value);
}
const NG_DEV_MODE$b = typeof ngDevMode === 'undefined' || ngDevMode;
const pathCompareMap = {
'exact': equalSegmentGroups,
'subset': containsSegmentGroup,
};
const paramCompareMap = {
'exact': equalParams,
'subset': containsParams,
'ignored': () => true,
};
function containsTree(container, containee, options) {
return pathCompareMap[options.paths](container.root, containee.root, options.matrixParams) &&
paramCompareMap[options.queryParams](container.queryParams, containee.queryParams) &&
!(options.fragment === 'exact' && container.fragment !== containee.fragment);
}
function equalParams(container, containee) {
// TODO: This does not handle array params correctly.
return shallowEqual(container, containee);
}
function equalSegmentGroups(container, containee, matrixParams) {
if (!equalPath(container.segments, containee.segments))
return false;
if (!matrixParamsMatch(container.segments, containee.segments, matrixParams)) {
return false;
}
if (container.numberOfChildren !== containee.numberOfChildren)
return false;
for (const c in containee.children) {
if (!container.children[c])
return false;
if (!equalSegmentGroups(container.children[c], containee.children[c], matrixParams))
return false;
}
return true;
}
function containsParams(container, containee) {
return Object.keys(containee).length <= Object.keys(container).length &&
Object.keys(containee).every(key => equalArraysOrString(container[key], containee[key]));
}
function containsSegmentGroup(container, containee, matrixParams) {
return containsSegmentGroupHelper(container, containee, containee.segments, matrixParams);
}
function containsSegmentGroupHelper(container, containee, containeePaths, matrixParams) {
if (container.segments.length > containeePaths.length) {
const current = container.segments.slice(0, containeePaths.length);
if (!equalPath(current, containeePaths))
return false;
if (containee.hasChildren())
return false;
if (!matrixParamsMatch(current, containeePaths, matrixParams))
return false;
return true;
}
else if (container.segments.length === containeePaths.length) {
if (!equalPath(container.segments, containeePaths))
return false;
if (!matrixParamsMatch(container.segments, containeePaths, matrixParams))
return false;
for (const c in containee.children) {
if (!container.children[c])
return false;
if (!containsSegmentGroup(container.children[c], containee.children[c], matrixParams)) {
return false;
}
}
return true;
}
else {
const current = containeePaths.slice(0, container.segments.length);
const next = containeePaths.slice(container.segments.length);
if (!equalPath(container.segments, current))
return false;
if (!matrixParamsMatch(container.segments, current, matrixParams))
return false;
if (!container.children[PRIMARY_OUTLET])
return false;
return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next, matrixParams);
}
}
function matrixParamsMatch(containerPaths, containeePaths, options) {
return containeePaths.every((containeeSegment, i) => {
return paramCompareMap[options](containerPaths[i].parameters, containeeSegment.parameters);
});
}
/**
* @description
*
* Represents the parsed URL.
*
* Since a router state is a tree, and the URL is nothing but a serialized state, the URL is a
* serialized tree.
* UrlTree is a data structure that provides a lot of affordances in dealing with URLs
*
* @usageNotes
* ### Example
*
* ```
* @Component({templateUrl:'template.html'})
* class MyComponent {
* constructor(router: Router) {
* const tree: UrlTree =
* router.parseUrl('/team/33/(user/victor//support:help)?debug=true#fragment');
* const f = tree.fragment; // return 'fragment'
* const q = tree.queryParams; // returns {debug: 'true'}
* const g: UrlSegmentGroup = tree.root.children[PRIMARY_OUTLET];
* const s: UrlSegment[] = g.segments; // returns 2 segments 'team' and '33'
* g.children[PRIMARY_OUTLET].segments; // returns 2 segments 'user' and 'victor'
* g.children['support'].segments; // return 1 segment 'help'
* }
* }
* ```
*
* @publicApi
*/
class UrlTree {
constructor(
/** The root segment group of the URL tree */
root = new UrlSegmentGroup([], {}),
/** The query params of the URL */
queryParams = {},
/** The fragment of the URL */
fragment = null) {
this.root = root;
this.queryParams = queryParams;
this.fragment = fragment;
if (NG_DEV_MODE$b) {
if (root.segments.length > 0) {
throw new ɵRuntimeError(4015 /* RuntimeErrorCode.INVALID_ROOT_URL_SEGMENT */, 'The root `UrlSegmentGroup` should not contain `segments`. ' +
'Instead, these segments belong in the `children` so they can be associated with a named outlet.');
}
}
}
get queryParamMap() {
if (!this._queryParamMap) {
this._queryParamMap = convertToParamMap(this.queryParams);
}
return this._queryParamMap;
}
/** @docsNotRequired */
toString() {
return DEFAULT_SERIALIZER.serialize(this);
}
}
/**
* @description
*
* Represents the parsed URL segment group.
*
* See `UrlTree` for more information.
*
* @publicApi
*/
class UrlSegmentGroup {
constructor(
/** The URL segments of this group. See `UrlSegment` for more information */
segments,
/** The list of children of this group */
children) {
this.segments = segments;
this.children = children;
/** The parent node in the url tree */
this.parent = null;
forEach(children, (v, k) => v.parent = this);
}
/** Whether the segment has child segments */
hasChildren() {
return this.numberOfChildren > 0;
}
/** Number of child segments */
get numberOfChildren() {
return Object.keys(this.children).length;
}
/** @docsNotRequired */
toString() {
return serializePaths(this);
}
}
/**
* @description
*
* Represents a single URL segment.
*
* A UrlSegment is a part of a URL between the two slashes. It contains a path and the matrix
* parameters associated with the segment.
*
* @usageNotes
* ### Example
*
* ```
* @Component({templateUrl:'template.html'})
* class MyComponent {
* constructor(router: Router) {
* const tree: UrlTree = router.parseUrl('/team;id=33');
* const g: UrlSegmentGroup = tree.root.children[PRIMARY_OUTLET];
* const s: UrlSegment[] = g.segments;
* s[0].path; // returns 'team'
* s[0].parameters; // returns {id: 33}
* }
* }
* ```
*
* @publicApi
*/
class UrlSegment {
constructor(
/** The path part of a URL segment */
path,
/** The matrix parameters associated with a segment */
parameters) {
this.path = path;
this.parameters = parameters;
}
get parameterMap() {
if (!this._parameterMap) {
this._parameterMap = convertToParamMap(this.parameters);
}
return this._parameterMap;
}
/** @docsNotRequired */
toString() {
return serializePath(this);
}
}
function equalSegments(as, bs) {
return equalPath(as, bs) && as.every((a, i) => shallowEqual(a.parameters, bs[i].parameters));
}
function equalPath(as, bs) {
if (as.length !== bs.length)
return false;
return as.every((a, i) => a.path === bs[i].path);
}
function mapChildrenIntoArray(segment, fn) {
let res = [];
forEach(segment.children, (child, childOutlet) => {
if (childOutlet === PRIMARY_OUTLET) {
res = res.concat(fn(child, childOutlet));
}
});
forEach(segment.children, (child, childOutlet) => {
if (childOutlet !== PRIMARY_OUTLET) {
res = res.concat(fn(child, childOutlet));
}
});
return res;
}
/**
* @description
*
* Serializes and deserializes a URL string into a URL tree.
*
* The url serialization strategy is customizable. You can
* make all URLs case insensitive by providing a custom UrlSerializer.
*
* See `DefaultUrlSerializer` for an example of a URL serializer.
*
* @publicApi
*/
class UrlSerializer {
}
UrlSerializer.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.1.5", ngImport: i0, type: UrlSerializer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
UrlSerializer.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.1.5", ngImport: i0, type: UrlSerializer, providedIn: 'root', useFactory: () => new DefaultUrlSerializer() });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.1.5", ngImport: i0, type: UrlSerializer, decorators: [{
type: Injectable,
args: [{ providedIn: 'root', useFactory: () => new DefaultUrlSerializer() }]
}] });
/**
* @description
*
* A default implementation of the `UrlSerializer`.
*
* Example URLs:
*
* ```
* /inbox/33(popup:compose)
* /inbox/33;open=true/messages/44
* ```
*
* DefaultUrlSerializer uses parentheses to serialize secondary segments (e.g., popup:compose), the
* colon syntax to specify the outlet, and the ';parameter=value' syntax (e.g., open=true) to
* specify route specific parameters.
*
* @publicApi
*/
class DefaultUrlSerializer {
/** Parses a url into a `UrlTree` */
parse(url) {
const p = new UrlParser(url);
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
}
/** Converts a `UrlTree` into a url */
serialize(tree) {
const segment = `/${serializeSegment(tree.root, true)}`;
const query = serializeQueryParams(tree.queryParams);
const fragment = typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : '';
return `${segment}${query}${fragment}`;
}
}
const DEFAULT_SERIALIZER = new DefaultUrlSerializer();
function serializePaths(segment) {
return segment.segments.map(p => serializePath(p)).join('/');
}
function serializeSegment(segment, root) {
if (!segment.hasChildren()) {
return serializePaths(segment);
}
if (root) {
const primary = segment.children[PRIMARY_OUTLET] ?
serializeSegment(segment.children[PRIMARY_OUTLET], false) :
'';
const children = [];
forEach(segment.children, (v, k) => {
if (k !== PRIMARY_OUTLET) {
children.push(`${k}:${serializeSegment(v, false)}`);
}
});
return children.length > 0 ? `${primary}(${children.join('//')})` : primary;
}
else {
const children = mapChildrenIntoArray(segment, (v, k) => {
if (k === PRIMARY_OUTLET) {
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
}
return [`${k}:${serializeSegment(v, false)}`];
});
// use no parenthesis if the only child is a primary outlet route
if (Object.keys(segment.children).length === 1 && segment.children[PRIMARY_OUTLET] != null) {
return `${serializePaths(segment)}/${children[0]}`;
}
return `${serializePaths(segment)}/(${children.join('//')})`;
}
}
/**
* Encodes a URI string with the default encoding. This function will only ever be called from
* `encodeUriQuery` or `encodeUriSegment` as it's the base set of encodings to be used. We need
* a custom encoding because encodeURIComponent is too aggressive and encodes stuff that doesn't
* have to be encoded per https://url.spec.whatwg.org.
*/
function encodeUriString(s) {
return encodeURIComponent(s)
.replace(/%40/g, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',');
}
/**
* This function should be used to encode both keys and values in a query string key/value. In
* the following URL, you need to call encodeUriQuery on "k" and "v":
*
* http://www.site.org/html;mk=mv?k=v#f
*/
function encodeUriQuery(s) {
return encodeUriString(s).replace(/%3B/gi, ';');
}
/**
* This function should be used to encode a URL fragment. In the following URL, you need to call
* encodeUriFragment on "f":
*
* http://www.site.org/html;mk=mv?k=v#f
*/
function encodeUriFragment(s) {
return encodeURI(s);
}
/**
* This function should be run on any URI segment as well as the key and value in a key/value
* pair for matrix params. In the following URL, you need to call encodeUriSegment on "html",
* "mk", and "mv":
*
* http://www.site.org/html;mk=mv?k=v#f
*/
function encodeUriSegment(s) {
return encodeUriString(s).replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/%26/gi, '&');
}
function decode(s) {
return decodeURIComponent(s);
}
// Query keys/values should have the "+" replaced first, as "+" in a query string is " ".
// decodeURIComponent function will not decode "+" as a space.
function decodeQuery(s) {
return decode(s.replace(/\+/g, '%20'));
}
function serializePath(path) {
return `${encodeUriSegment(path.path)}${serializeMatrixParams(path.parameters)}`;
}
function serializeMatrixParams(params) {
return Object.keys(params)
.map(key => `;${encodeUriSegment(key)}=${encodeUriSegment(params[key])}`)
.join('');
}
function serializeQueryParams(params) {
const strParams = Object.keys(params)
.map((name) => {
const value = params[name];
return Array.isArray(value) ?
value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') :
`${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
})
.filter(s => !!s);
return strParams.length ? `?${strParams.join('&')}` : '';
}
const SEGMENT_RE = /^[^\/()?;=#]+/;
function matchSegments(str) {
const match = str.match(SEGMENT_RE);
return match ? match[0] : '';
}
const QUERY_PARAM_RE = /^[^=?&#]+/;
// Return the name of the query param at the start of the string or an empty string
function matchQueryParams(str) {
const match = str.match(QUERY_PARAM_RE);
return match ? match[0] : '';
}
const QUERY_PARAM_VALUE_RE = /^[^&#]+/;
// Return the value of the query param at the start of the string or an empty string
function matchUrlQueryParamValue(str) {
const match = str.match(QUERY_PARAM_VALUE_RE);
return match ? match[0] : '';
}
class UrlParser {
constructor(url) {
this.url = url;
this.remaining = url;
}
parseRootSegment() {
this.consumeOptional('/');
if (this.remaining === '' || this.peekStartsWith('?') || this.peekStartsWith('#')) {
return new UrlSegmentGroup([], {});
}
// The root segment group never has segments
return new UrlSegmentGroup([], this.parseChildren());
}
parseQueryParams() {
const params = {};
if (this.consumeOptional('?')) {
do {
this.parseQueryParam(params);
} while (this.consumeOptional('&'));
}
return params;
}
parseFragment() {
return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null;
}
parseChildren() {
if (this.remaining === '') {
return {};
}
this.consumeOptional('/');
const segments = [];
if (!this.peekStartsWith('(')) {
segments.push(this.parseSegment());
}
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
this.capture('/');
segments.push(this.parseSegment());
}
let children = {};
if (this.peekStartsWith('/(')) {
this.capture('/');
children = this.parseParens(true);
}
let res = {};
if (this.peekStartsWith('(')) {
res = this.parseParens(false);
}
if (segments.length > 0 || Object.keys(children).length > 0) {
res[PRIMARY_OUTLET] = new UrlSegmentGroup(segments, children);
}
return res;
}
// parse a segment with its matrix parameters
// ie `name;k1=v1;k2`
parseSegment() {
const path = matchSegments(this.remaining);
if (path === '' && this.peekStartsWith(';')) {
throw new ɵRuntimeError(4009 /* RuntimeErrorCode.EMPTY_PATH_WITH_PARAMS */, NG_DEV_MODE$b && `Empty path url segment cannot have parameters: '${this.remaining}'.`);
}
this.capture(path);
return new UrlSegment(decode(path), this.parseMatrixParams());
}
parseMatrixParams() {
const params = {};
while (this.consumeOptional(';')) {
this.parseParam(params);
}
return params;
}
parseParam(params) {
const key = matchSegments(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value = '';
if (this.consumeOptional('=')) {
const valueMatch = matchSegments(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
}
}
params[decode(key)] = decode(value);
}
// Parse a single query parameter `name[=value]`
parseQueryParam(params) {
const key = matchQueryParams(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value = '';
if (this.consumeOptional('=')) {
const valueMatch = matchUrlQueryParamValue(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
}
}
const decodedKey = decodeQuery(key);
const decodedVal = decodeQuery(value);
if (params.hasOwnProperty(decodedKey)) {
// Append to existing values
let currentVal = params[decodedKey];
if (!Array.isArray(currentVal)) {
currentVal = [currentVal];
params[decodedKey] = currentVal;
}
currentVal.push(decodedVal);
}
else {
// Create a new value
params[decodedKey] = decodedVal;
}
}
// parse `(a/b//outlet_name:c/d)`
parseParens(allowPrimary) {
const segments = {};
this.capture('(');
while (!this.consumeOptional(')') && this.remaining.length > 0) {
const path = matchSegments(this.remaining);
const next = this.remaining[path.length];
// if is is not one of these characters, then the segment was unescaped
// or the group was not closed
if (next !== '/' && next !== ')' && next !== ';') {
throw new ɵRuntimeError(4010 /* RuntimeErrorCode.UNPARSABLE_URL */, NG_DEV_MODE$b && `Cannot parse url '${this.url}'`);
}
let outletName = undefined;
if (path.indexOf(':') > -1) {
outletName = path.slice(0, path.indexOf(':'));
this.capture(outletName);
this.capture(':');
}
else if (allowPrimary) {
outletName = PRIMARY_OUTLET;
}
const children = this.parseChildren();
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
new UrlSegmentGroup([], children);
this.consumeOptional('//');
}
return segments;
}
peekStartsWith(str) {
return this.remaining.startsWith(str);
}
// Consumes the prefix when it is present and returns whether it has been consumed
consumeOptional(str) {
if (this.peekStartsWith(str)) {
this.remaining = this.remaining.substring(str.length);
return true;
}
return false;
}
capture(str) {
if (!this.consumeOptional(str)) {
throw new ɵRuntimeError(4011 /* RuntimeErrorCode.UNEXPECTED_VALUE_IN_URL */, NG_DEV_MODE$b && `Expected "${str}".`);
}
}
}
function createRoot(rootCandidate) {
return rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], { [PRIMARY_OUTLET]: rootCandidate }) :
rootCandidate;
}
/**
* Recursively merges primary segment children into their parents and also drops empty children
* (those which have no segments and no children themselves). The latter prevents serializing a
* group into something like `/a(aux:)`, where `aux` is an empty child segment.
*/
function squashSegmentGroup(segmentGroup) {
const newChildren = {};
for (const childOutlet of Object.keys(segmentGroup.children)) {
const child = segmentGroup.children[childOutlet];
const childCandidate = squashSegmentGroup(child);
// don't add empty children
if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) {
newChildren[childOutlet] = childCandidate;
}
}
const s = new UrlSegmentGroup(segmentGroup.segments, newChildren);
return mergeTrivialChildren(s);
}
/**
* When possible, merges the primary outlet child into the parent `UrlSegmentGroup`.
*
* When a segment group has only one child which is a primary outlet, merges that child into the
* parent. That is, the child segment group's segments are merged into the `s` and the child's
* children become the children of `s`. Think of this like a 'squash', merging the child segment
* group into the parent.
*/
function mergeTrivialChildren(s) {
if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) {
const c = s.children[PRIMARY_OUTLET];
return new UrlSegmentGroup(s.segments.concat(c.segments), c.children);
}
return s;
}
function isUrlTree(v) {
return v instanceof UrlTree;
}
const NG_DEV_MODE$a = typeof ngDevMode === 'undefined' || ngDevMode;
/**
* Creates a `UrlTree` relative to an `ActivatedRouteSnapshot`.
*
* @publicApi
*
*
* @param relativeTo The `ActivatedRouteSnapshot` to apply the commands to
* @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 one provided in the `relativeTo` parameter.
* @param queryParams The query parameters for the `UrlTree`. `null` if the `UrlTree` does not have
* any query parameters.
* @param fragment The fragment for the `UrlTree`. `null` if the `UrlTree` does not have a fragment.
*
* @usageNotes
*
* ```
* // create /team/33/user/11
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, 'user', 11]);
*
* // create /team/33;expand=true/user/11
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, {expand: true}, 'user', 11]);
*
* // you can collapse static segments like this (this works only with the first passed-in value):
* createUrlTreeFromSnapshot(snapshot, ['/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:
* createUrlTreeFromSnapshot(snapshot, [{segmentPath: '/one/two'}]);
*
* // create /team/33/(user/11//right:chat)
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, {outlets: {primary: 'user/11', right:
* 'chat'}}], null, null);
*
* // remove the right secondary node
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, {outlets: {primary: 'user/11', right: null}}]);
*
* // For the examples below, assume the current URL is for the `/team/33/user/11` and the
* `ActivatedRouteSnapshot` points to `user/11`:
*
* // navigate to /team/33/user/11/details
* createUrlTreeFromSnapshot(snapshot, ['details']);
*
* // navigate to /team/33/user/22
* createUrlTreeFromSnapshot(snapshot, ['../22']);
*
* // navigate to /team/44/user/22
* createUrlTreeFromSnapshot(snapshot, ['../../team/44/user/22']);
* ```
*/
function createUrlTreeFromSnapshot(relativeTo, commands, queryParams = null, fragment = null) {
const relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeTo);
return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment);
}
function createSegmentGroupFromRoute(route) {
let targetGroup;
function createSegmentGroupFromRouteRecursive(currentRoute) {
const childOutlets = {};
for (const childSnapshot of currentRoute.children) {
const root = createSegmentGroupFromRouteRecursive(childSnapshot);
childOutlets[childSnapshot.outlet] = root;
}
const segmentGroup = new UrlSegmentGroup(currentRoute.url, childOutlets);
if (currentRoute === route) {
targetGroup = segmentGroup;
}
return segmentGroup;
}
const rootCandidate = createSegmentGroupFromRouteRecursive(route.root);
const rootSegmentGroup = createRoot(rootCandidate);
return targetGroup ?? rootSegmentGroup;
}
function createUrlTreeFromSegmentGroup(relativeTo, commands, queryParams, fragment) {
let root = relativeTo;
while (root.parent) {
root = root.parent;
}
// There are no commands so the `UrlTree` goes to the same path as the one created from the
// `UrlSegmentGroup`. All we need to do is update the `queryParams` and `fragment` without
// applying any other logic.
if (commands.length === 0) {
return tree(root, root, root, queryParams, fragment);
}
const nav = computeNavigation(commands);
if (nav.toRoot()) {
return tree(root, root, new UrlSegmentGroup([], {}), queryParams, fragment);
}
const position = findStartingPositionForTargetGroup(nav, root, relativeTo);
const newSegmentGroup = position.processChildren ?
updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands) :
updateSegmentGroup(position.segmentGroup, position.index, nav.commands);
return tree(root, position.segmentGroup, newSegmentGroup, queryParams, fragment);
}
function createUrlTree(route, urlTree, commands, queryParams, fragment) {
if (commands.length === 0) {
return tree(urlTree.root, urlTree.root, urlTree.root, queryParams, fragment);
}
const nav = computeNavigation(commands);
if (nav.toRoot()) {
return tree(urlTree.root, urlTree.root, new UrlSegmentGroup([], {}), queryParams, fragment);
}
function createTreeUsingPathIndex(lastPathIndex) {
const startingPosition = findStartingPosition(nav, urlTree, route.snapshot?._urlSegment, lastPathIndex);
const segmentGroup = startingPosition.processChildren ?
updateSegmentGroupChildren(startingPosition.segmentGroup, startingPosition.index, nav.commands) :
updateSegmentGroup(startingPosition.segmentGroup, startingPosition.index, nav.commands);
return tree(urlTree.root, startingPosition.segmentGroup, segmentGroup, queryParams, fragment);
}
// Note: The types should disallow `snapshot` from being `undefined` but due to test mocks, this
// may be the case. Since we try to access it at an earlier point before the refactor to add the
// warning for `relativeLinkResolution: 'legacy'`, this may cause failures in tests where it
// didn't before.
const result = createTreeUsingPathIndex(route.snapshot?._lastPathIndex);
return result;
}
function isMatrixParams(command) {
return typeof command === 'object' && command != null && !command.outlets && !command.segmentPath;
}
/**
* Determines if a given command has an `outlets` map. When we encounter a command
* with an outlets k/v map, we need to apply each outlet individually to the existing segment.
*/
function isCommandWithOutlets(command) {
return typeof command === 'object' && command != null && command.outlets;
}
function tree(oldRoot, oldSegmentGroup, newSegmentGroup, queryParams, fragment) {
let qp = {};
if (queryParams) {
forEach(queryParams, (value, name) => {
qp[name] = Array.isArray(value) ? value.map((v) => `${v}`) : `${value}`;
});
}
let rootCandidate;
if (oldRoot === oldSegmentGroup) {
rootCandidate = newSegmentGroup;
}
else {
rootCandidate = replaceSegment(oldRoot, oldSegmentGroup, newSegmentGroup);
}
const newRoot = createRoot(squashSegmentGroup(rootCandidate));
return new UrlTree(newRoot, qp, fragment);
}
/**
* Replaces the `oldSegment` which is located in some child of the `current` with the `newSegment`.
* This also has the effect of creating new `UrlSegmentGroup` copies to update references. This
* shouldn't be necessary but the fallback logic for an invalid ActivatedRoute in the creation uses
* the Router's current url tree. If we don't create new segment groups, we end up modifying that
* value.
*/
function replaceSegment(current, oldSegment, newSegment) {
const children = {};
forEach(current.children, (c, outletName) => {
if (c === oldSegment) {
children[outletName] = newSegment;
}
else {
children[outletName] = replaceSegment(c, oldSegment, newSegment);
}
});
return new UrlSegmentGroup(current.segments, children);
}
class Navigation {
constructor(isAbsolute, numberOfDoubleDots, commands) {
this.isAbsolute = isAbsolute;
this.numberOfDoubleDots = numberOfDoubleDots;
this.commands = commands;
if (isAbsolute && commands.length > 0 && isMatrixParams(commands[0])) {
throw new ɵRuntimeError(4003 /* RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS */, NG_DEV_MODE$a && 'Root segment cannot have matrix parameters');
}
const cmdWithOutlet = commands.find(isCommandWithOutlets);
if (cmdWithOutlet && cmdWithOutlet !== last(commands)) {
throw new ɵRuntimeError(4004 /* RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND */, NG_DEV_MODE$a && '{outlets:{}} has to be the last command');
}
}
toRoot() {
return this.isAbsolute && this.commands.length === 1 && this.commands[0] == '/';
}
}
/** Transforms commands to a normalized `Navigation` */
function computeNavigation(commands) {
if ((typeof commands[0] === 'string') && commands.length === 1 && commands[0] === '/') {
return new Navigation(true, 0, commands);
}
let numberOfDoubleDots = 0;
let isAbsolute = false;
const res = commands.reduce((res, cmd, cmdIdx) => {
if (typeof cmd === 'object' && cmd != null) {
if (cmd.outlets) {
const outlets = {};
forEach(cmd.outlets, (commands, name) => {
outlets[name] = typeof commands === 'string' ? commands.split('/') : commands;
});
return [...res, { outlets }];
}
if (cmd.segmentPath) {
return [...res, cmd.segmentPath];
}
}
if (!(typeof cmd === 'string')) {
return [...res, cmd];
}
if (cmdIdx === 0) {
cmd.split('/').forEach((urlPart, partIndex) => {
if (partIndex == 0 && urlPart === '.') {
// skip './a'
}
else if (partIndex == 0 && urlPart === '') { // '/a'
isAbsolute = true;
}
else if (urlPart === '..') { // '../a'
numberOfDoubleDots++;
}
else if (urlPart != '') {
res.push(urlPart);
}
});
return res;
}
return [...res, cmd];
}, []);
return new Navigation(isAbsolute, numberOfDoubleDots, res);
}
class Position {
constructor(segmentGroup, processChildren, index) {
this.segmentGroup = segmentGroup;
this.processChildren = processChildren;
this.index = index;
}
}
function findStartingPositionForTargetGroup(nav, root, target) {
if (nav.isAbsolute) {
return new Position(root, true, 0);
}
if (!target) {
// `NaN` is used only to maintain backwards compatibility with incorrectly mocked
// `ActivatedRouteSnapshot` in tests. In prior versions of this code, the position here was
// determined based on an internal property that was rarely mocked, resulting in `NaN`. In
// reality, this code path should _never_ be touched since `target` is not allowed to be falsey.
return new Position(root, false, NaN);
}
if (target.parent === null) {
return new Position(target, true, 0);
}
const modifier = isMatrixParams(nav.commands[0]) ? 0 : 1;
const index = target.segments.length - 1 + modifier;
return createPositionApplyingDoubleDots(target, index, nav.numberOfDoubleDots);
}
function findStartingPosition(nav, tree, segmentGroup, lastPathIndex) {
if (nav.isAbsolute) {
return new Position(tree.root, true, 0);
}
if (lastPathIndex === -1) {
// Pathless ActivatedRoute has _lastPathIndex === -1 but should not process children
// see issue #26224, #13011, #35687
// However, if the ActivatedRoute is the root we should process children like above.
const processChildren = segmentGroup === tree.root;
return new Position(segmentGroup, processChildren, 0);
}
const modifier = isMatrixParams(nav.commands[0]) ? 0 : 1;
const index = lastPathIndex + modifier;
return createPositionApplyingDoubleDots(segmentGroup, index, nav.numberOfDoubleDots);
}
function createPositionApplyingDoubleDots(group, index, numberOfDoubleDots) {
let g = group;
let ci = index;
let dd = numberOfDoubleDots;
while (dd > ci) {
dd -= ci;
g = g.parent;
if (!g) {
throw new ɵRuntimeError(4005 /* RuntimeErrorCode.INVALID_DOUBLE_DOTS */, NG_DEV_MODE$a && 'Invalid number of \'../\'');
}
ci = g.segments.length;
}
return new Position(g, false, ci - dd);
}
function getOutlets(commands) {
if (isCommandWithOutlets(commands[0])) {
return commands[0].outlets;
}
return { [PRIMARY_OUTLET]: commands };
}
function updateSegmentGroup(segmentGroup, startIndex, commands) {
if (!segmentGroup) {
segmentGroup = new UrlSegmentGroup([], {});
}
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return updateSegmentGroupChildren(segmentGroup, startIndex, commands);
}
const m = prefixedWith(segmentGroup, startIndex, commands);
const slicedCommands = commands.slice(m.commandIndex);
if (m.match && m.pathIndex < segmentGroup.segments.length) {
const g = new UrlSegmentGroup(segmentGroup.segments.slice(0, m.pathIndex), {});
g.children[PRIMARY_OUTLET] =
new UrlSegmentGroup(segmentGroup.segments.slice(m.pathIndex), segmentGroup.children);
return updateSegmentGroupChildren(g, 0, slicedCommands);
}
else if (m.match && slicedCommands.length === 0) {
return new UrlSegmentGroup(segmentGroup.segments, {});
}
else if (m.match && !segmentGroup.hasChildren()) {
return createNewSegmentGroup(segmentGroup, startIndex, commands);
}
else if (m.match) {
return updateSegmentGroupChildren(segmentGroup, 0, slicedCommands);
}
else {
return createNewSegmentGroup(segmentGroup, startIndex, commands);
}
}
function updateSegmentGroupChildren(segmentGroup, startIndex, commands) {
if (commands.length === 0) {
return new UrlSegmentGroup(segmentGroup.segments, {});
}
else {
const outlets = getOutlets(commands);
const children = {};
// If the set of commands does not apply anything to the primary outlet and the child segment is
// an empty path primary segment on its own, we want to skip applying the commands at this
// level. Imagine the following config:
//
// {path: '', children: [{path: '**', outlet: 'popup'}]}.
//
// Navigation to /(popup:a) will activate the child outlet correctly Given a follow-up
// navigation with commands
// ['/', {outlets: {'popup': 'b'}}], we _would not_ want to apply the outlet commands to the
// root segment because that would result in
// //(popup:a)(popup:b) since the outlet command got applied one level above where it appears in
// the `ActivatedRoute` rather than updating the existing one.
//
// Because empty paths do not appear in the URL segments and the fact that the segments used in
// the output `UrlTree` are squashed to eliminate these empty paths where possible
// https://github.com/angular/angular/blob/13f10de40e25c6900ca55bd83b36bd533dacfa9e/packages/router/src/url_tree.ts#L755
// it can be hard to determine what is the right thing to do when applying commands to a
// `UrlSegmentGroup` that is created from an "unsquashed"/expanded `ActivatedRoute` tree.
// This code effectively "squashes" empty path primary routes when they have no siblings on
// the same level of the tree.
if (!outlets[PRIMARY_OUTLET] && segmentGroup.children[PRIMARY_OUTLET] &&
segmentGroup.numberOfChildren === 1 &&
segmentGroup.children[PRIMARY_OUTLET].segments.length === 0) {
return updateSegmentGroupChildren(segmentGroup.children[PRIMARY_OUTLET], startIndex, commands);
}
forEach(outlets, (commands, outlet) => {
if (typeof commands === 'string') {
commands = [commands];
}
if (commands !== null) {
children[outlet] = updateSegmentGroup(segmentGroup.children[outlet], startIndex, commands);
}
});
forEach(segmentGroup.children, (child, childOutlet) => {
if (outlets[childOutlet] === undefined) {
children[childOutlet] = child;
}
});
return new UrlSegmentGroup(segmentGroup.segments, children);
}
}
function prefixedWith(segmentGroup, startIndex, commands) {
let currentCommandIndex = 0;
let currentPathIndex = startIndex;
const noMatch = { match: false, pathIndex: 0, commandIndex: 0 };
while (currentPathIndex < segmentGroup.segments.length) {
if (currentCommandIndex >= commands.length)
return noMatch;
const path = segmentGroup.segments[currentPathIndex];
const command = commands[currentCommandIndex];
// Do not try to consume command as part of the prefixing if it has outlets because it can
// contain outlets other than the one being processed. Consuming the outlets command would
// result in other outlets being ignored.
if (isCommandWithOutlets(command)) {
break;
}
const curr = `${command}`;
const next = currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
if (currentPathIndex > 0 && curr === undefined)
break;
if (curr && next && (typeof next === 'object') && next.outlets === undefined) {
if (!compare(curr, next, path))
return noMatch;
currentCommandIndex += 2;
}
else {
if (!compare(curr, {}, path))
return noMatch;
currentCommandIndex++;
}
currentPathIndex++;
}
return { match: true, pathIndex: currentPathIndex, commandIndex: currentCommandIndex };
}
function createNewSegmentGroup(segmentGroup, startIndex, commands) {
const paths = segmentGroup.segments.slice(0, startIndex);
let i = 0;
while (i < commands.length) {
const command = commands[i];
if (isCommandWithOutlets(command)) {
const children = createNewSegmentChildren(command.outlets);
return new UrlSegmentGroup(paths, children);
}
// if we start with an object literal, we need to reuse the path part from the segment
if (i === 0 && isMatrixParams(commands[0])) {
const p = segmentGroup.segments[startIndex];
paths.push(new UrlSegment(p.path, stringify(commands[0])));
i++;
continue;
}
const curr = isCommandWithOutlets(command) ? command.outlets[PRIMARY_OUTLET] : `${command}`;
const next = (i < commands.length - 1) ? commands[i + 1] : null;
if (curr && next && isMatrixParams(next)) {
paths.push(new UrlSegment(curr, stringify(next)));
i += 2;
}
else {
paths.push(new UrlSegment(curr, {}));
i++;
}
}
return new UrlSegmentGroup(paths, {});
}
function createNewSegmentChildren(outlets) {
const children = {};
forEach(outlets, (commands, outlet) => {
if (typeof commands === 'string') {
commands = [commands];
}
if (commands !== null) {
children[outlet] = createNewSegmentGroup(new UrlSegmentGroup([], {}), 0, commands);
}
});
return children;
}
function stringify(params) {
const res = {};
forEach(params, (v, k) => res[k] = `${v}`);
return res;
}
function compare(path, params, segment) {
return path == segment.path && shallowEqual(params, segment.parameters);
}
const IMPERATIVE_NAVIGATION = 'imperative';
/**
* Base for events the router goes through, as opposed to events tied to a specific
* route. Fired one time for any given navigation.
*
* The following code shows how a class subscribes to router events.
*
* ```ts
* import {Event, RouterEvent, Router} from '@angular/router';
*
* class MyService {
* constructor(public router: Router) {
* router.events.pipe(
* filter((e: Event): e is RouterEvent => e instanceof RouterEvent)
* ).subscribe((e: RouterEvent) => {
* // Do something
* });
* }
* }
* ```
*
* @see `Event`
* @see [Router events summary](guide/router-reference#router-events)
* @publicApi
*/
class RouterEvent {
constructor(
/** A unique ID that the router assigns to every router navigation. */
id,
/** The URL that is the destination for this navigation. */
url) {
this.id = id;
this.url = url;
}
}
/**
* An event triggered when a navigation starts.
*
* @publicApi
*/
class NavigationStart extends RouterEvent {
constructor(
/** @docsNotRequired */
id,
/** @docsNotRequired */
url,
/** @docsNotRequired */
navigationTrigger = 'imperative',
/** @docsNotRequired */
restoredState = null) {
super(id, url);
this.type = 0 /* EventType.NavigationStart */;
this.navigationTrigger = navigationTrigger;
this.restoredState = restoredState;
}
/** @docsNotRequired */
toString() {
return `NavigationStart(id: ${this.id}, url: '${this.url}')`;
}
}
/**
* An event triggered when a navigation ends successfully.
*
* @see `NavigationStart`
* @see `NavigationCancel`
* @see `NavigationError`
*
* @publicApi
*/
class NavigationEnd extends RouterEvent {
constructor(
/** @docsNotRequired */
id,
/** @docsNotRequired */
url,
/** @docsNotRequired */
urlAfterRedirects) {
super(id, url);
this.urlAfterRedirects = urlAfterRedirects;
this.type = 1 /* EventType.NavigationE