UNPKG

@angular/router

Version:
1,585 lines (1,575 loc) 209 kB
/** * @license Angular v10.0.4 * (c) 2010-2020 Google LLC. https://angular.io/ * License: MIT */ import { Location, LocationStrategy, ViewportScroller, PlatformLocation, APP_BASE_HREF, HashLocationStrategy, PathLocationStrategy, ɵgetDOM, LOCATION_INITIALIZED } from '@angular/common'; import { Component, ɵisObservable, ɵisPromise, NgModuleRef, InjectionToken, NgModuleFactory, ɵConsole, NgZone, isDevMode, Injectable, Type, Injector, NgModuleFactoryLoader, Compiler, Directive, Attribute, Renderer2, ElementRef, Input, HostListener, HostBinding, ChangeDetectorRef, Optional, ContentChildren, EventEmitter, ViewContainerRef, ComponentFactoryResolver, Output, SystemJsNgModuleLoader, NgProbeToken, ANALYZE_FOR_ENTRY_COMPONENTS, SkipSelf, Inject, APP_INITIALIZER, APP_BOOTSTRAP_LISTENER, NgModule, ApplicationRef, Version } from '@angular/core'; import { of, from, BehaviorSubject, Observable, EmptyError, combineLatest, defer, EMPTY, Subject } from 'rxjs'; import { map, concatAll, last as last$1, catchError, first, mergeMap, tap, every, switchMap, take, startWith, scan, filter, concatMap, takeLast, finalize, mergeAll } from 'rxjs/operators'; /** * @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 */ /** * Base for events the router goes through, as opposed to events tied to a specific * route. Fired one time for any given navigation. * * @usageNotes * * ```ts * class MyService { * constructor(public router: Router, logger: Logger) { * router.events.pipe( * filter((e: Event): e is RouterEvent => e instanceof RouterEvent) * ).subscribe((e: RouterEvent) => { * logger.log(e.id, e.url); * }); * } * } * ``` * * @see `Event` * @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.navigationTrigger = navigationTrigger; this.restoredState = restoredState; } /** @docsNotRequired */ toString() { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; } } /** * An event triggered when a navigation ends successfully. * * @publicApi */ class NavigationEnd extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ urlAfterRedirects) { super(id, url); this.urlAfterRedirects = urlAfterRedirects; } /** @docsNotRequired */ toString() { return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`; } } /** * An event triggered when a navigation is canceled, directly or indirectly. * * This can happen when a [route guard](guide/router-tutorial-toh#milestone-5-route-guards) * returns `false` or initiates a redirect by returning a `UrlTree`. * * @publicApi */ class NavigationCancel extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ reason) { super(id, url); this.reason = reason; } /** @docsNotRequired */ toString() { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; } } /** * An event triggered when a navigation fails due to an unexpected error. * * @publicApi */ class NavigationError extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ error) { super(id, url); this.error = error; } /** @docsNotRequired */ toString() { return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`; } } /** *An event triggered when routes are recognized. * * @publicApi */ class RoutesRecognized extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ urlAfterRedirects, /** @docsNotRequired */ state) { super(id, url); this.urlAfterRedirects = urlAfterRedirects; this.state = state; } /** @docsNotRequired */ toString() { return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } /** * An event triggered at the start of the Guard phase of routing. * * @publicApi */ class GuardsCheckStart extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ urlAfterRedirects, /** @docsNotRequired */ state) { super(id, url); this.urlAfterRedirects = urlAfterRedirects; this.state = state; } toString() { return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } /** * An event triggered at the end of the Guard phase of routing. * * @publicApi */ class GuardsCheckEnd extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ urlAfterRedirects, /** @docsNotRequired */ state, /** @docsNotRequired */ shouldActivate) { super(id, url); this.urlAfterRedirects = urlAfterRedirects; this.state = state; this.shouldActivate = shouldActivate; } toString() { return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`; } } /** * An event triggered at the the start of the Resolve phase of routing. * * Runs in the "resolve" phase whether or not there is anything to resolve. * In future, may change to only run when there are things to be resolved. * * @publicApi */ class ResolveStart extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ urlAfterRedirects, /** @docsNotRequired */ state) { super(id, url); this.urlAfterRedirects = urlAfterRedirects; this.state = state; } toString() { return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } /** * An event triggered at the end of the Resolve phase of routing. * @see `ResolveStart`. * * @publicApi */ class ResolveEnd extends RouterEvent { constructor( /** @docsNotRequired */ id, /** @docsNotRequired */ url, /** @docsNotRequired */ urlAfterRedirects, /** @docsNotRequired */ state) { super(id, url); this.urlAfterRedirects = urlAfterRedirects; this.state = state; } toString() { return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } /** * An event triggered before lazy loading a route configuration. * * @publicApi */ class RouteConfigLoadStart { constructor( /** @docsNotRequired */ route) { this.route = route; } toString() { return `RouteConfigLoadStart(path: ${this.route.path})`; } } /** * An event triggered when a route has been lazy loaded. * * @publicApi */ class RouteConfigLoadEnd { constructor( /** @docsNotRequired */ route) { this.route = route; } toString() { return `RouteConfigLoadEnd(path: ${this.route.path})`; } } /** * An event triggered at the start of the child-activation * part of the Resolve phase of routing. * @see `ChildActivationEnd` * @see `ResolveStart` * * @publicApi */ class ChildActivationStart { constructor( /** @docsNotRequired */ snapshot) { this.snapshot = snapshot; } toString() { const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; return `ChildActivationStart(path: '${path}')`; } } /** * An event triggered at the end of the child-activation part * of the Resolve phase of routing. * @see `ChildActivationStart` * @see `ResolveStart` * * @publicApi */ class ChildActivationEnd { constructor( /** @docsNotRequired */ snapshot) { this.snapshot = snapshot; } toString() { const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; return `ChildActivationEnd(path: '${path}')`; } } /** * An event triggered at the start of the activation part * of the Resolve phase of routing. * @see ActivationEnd` * @see `ResolveStart` * * @publicApi */ class ActivationStart { constructor( /** @docsNotRequired */ snapshot) { this.snapshot = snapshot; } toString() { const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; return `ActivationStart(path: '${path}')`; } } /** * An event triggered at the end of the activation part * of the Resolve phase of routing. * @see `ActivationStart` * @see `ResolveStart` * * @publicApi */ class ActivationEnd { constructor( /** @docsNotRequired */ snapshot) { this.snapshot = snapshot; } toString() { const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; return `ActivationEnd(path: '${path}')`; } } /** * An event triggered by scrolling. * * @publicApi */ class Scroll { constructor( /** @docsNotRequired */ routerEvent, /** @docsNotRequired */ position, /** @docsNotRequired */ anchor) { this.routerEvent = routerEvent; this.position = position; this.anchor = anchor; } toString() { const pos = this.position ? `${this.position[0]}, ${this.position[1]}` : null; return `Scroll(anchor: '${this.anchor}', position: '${pos}')`; } } /** * @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 */ /** * This component is used internally within the router to be a placeholder when an empty * router-outlet is needed. For example, with a config such as: * * `{path: 'parent', outlet: 'nav', children: [...]}` * * In order to render, there needs to be a component on this config, which will default * to this `EmptyOutletComponent`. */ class ɵEmptyOutletComponent { } ɵEmptyOutletComponent.decorators = [ { type: Component, args: [{ template: `<router-outlet></router-outlet>` },] } ]; /** * @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 */ /** * The primary routing outlet. * * @publicApi */ const PRIMARY_OUTLET = 'primary'; 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); } const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError'; function navigationCancelingError(message) { const error = Error('NavigationCancelingError: ' + message); error[NAVIGATION_CANCELING_ERROR] = true; return error; } function isNavigationCancelingError(error) { return error && error[NAVIGATION_CANCELING_ERROR]; } // Matches the route configuration (`route`) against the actual URL (`segments`). 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 }; } /** * @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 */ class LoadedRouterConfig { constructor(routes, module) { this.routes = routes; this.module = module; } } function validateConfig(config, parentPath = '') { // forEach doesn't iterate undefined values for (let i = 0; i < config.length; i++) { const route = config[i]; const fullPath = getFullPath(parentPath, route); validateNode(route, fullPath); } } function validateNode(route, fullPath) { if (!route) { throw new Error(` Invalid configuration of route '${fullPath}': Encountered undefined route. The reason might be an extra comma. Example: const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent },, << two commas { path: 'detail/:id', component: HeroDetailComponent } ]; `); } if (Array.isArray(route)) { throw new Error(`Invalid configuration of route '${fullPath}': Array cannot be specified`); } if (!route.component && !route.children && !route.loadChildren && (route.outlet && route.outlet !== PRIMARY_OUTLET)) { throw new Error(`Invalid configuration of route '${fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`); } if (route.redirectTo && route.children) { throw new Error(`Invalid configuration of route '${fullPath}': redirectTo and children cannot be used together`); } if (route.redirectTo && route.loadChildren) { throw new Error(`Invalid configuration of route '${fullPath}': redirectTo and loadChildren cannot be used together`); } if (route.children && route.loadChildren) { throw new Error(`Invalid configuration of route '${fullPath}': children and loadChildren cannot be used together`); } if (route.redirectTo && route.component) { throw new Error(`Invalid configuration of route '${fullPath}': redirectTo and component cannot be used together`); } if (route.path && route.matcher) { throw new Error(`Invalid configuration of route '${fullPath}': path and matcher cannot be used together`); } if (route.redirectTo === void 0 && !route.component && !route.children && !route.loadChildren) { throw new Error(`Invalid configuration of route '${fullPath}'. One of the following must be provided: component, redirectTo, children or loadChildren`); } if (route.path === void 0 && route.matcher === void 0) { throw new Error(`Invalid configuration of route '${fullPath}': routes must have either a path or a matcher specified`); } if (typeof route.path === 'string' && route.path.charAt(0) === '/') { throw new Error(`Invalid configuration of route '${fullPath}': path cannot start with a slash`); } if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) { const exp = `The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`; throw new Error(`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`); } if (route.pathMatch !== void 0 && route.pathMatch !== 'full' && route.pathMatch !== 'prefix') { throw new Error(`Invalid configuration of route '${fullPath}': pathMatch can only be set to 'prefix' or 'full'`); } if (route.children) { validateConfig(route.children, fullPath); } } function getFullPath(parentPath, currentRoute) { if (!currentRoute) { return parentPath; } if (!parentPath && !currentRoute.path) { return ''; } else if (parentPath && !currentRoute.path) { return `${parentPath}/`; } else if (!parentPath && currentRoute.path) { return currentRoute.path; } else { return `${parentPath}/${currentRoute.path}`; } } /** * Makes a copy of the config and adds any default required properties. */ function standardizeConfig(r) { const children = r.children && r.children.map(standardizeConfig); const c = children ? Object.assign(Object.assign({}, r), { children }) : Object.assign({}, r); if (!c.component && (children || c.loadChildren) && (c.outlet && c.outlet !== PRIMARY_OUTLET)) { c.component = ɵEmptyOutletComponent; } return c; } /** * @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 */ 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) { // Casting Object.keys return values to include `undefined` as there are some cases // in IE 11 where this can happen. Cannot provide a test because the behavior only // exists in certain circumstances in IE 11, therefore doing this cast ensures the // logic is correct for when this edge case is hit. const k1 = Object.keys(a); const k2 = Object.keys(b); 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; return a.every(aItem => b.indexOf(aItem) > -1); } 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 waitForMap(obj, fn) { if (Object.keys(obj).length === 0) { return of({}); } const waitHead = []; const waitTail = []; const res = {}; forEach(obj, (a, k) => { const mapped = fn(k, a).pipe(map((r) => res[k] = r)); if (k === PRIMARY_OUTLET) { waitHead.push(mapped); } else { waitTail.push(mapped); } }); // Closure compiler has problem with using spread operator here. So we use "Array.concat". // Note that we also need to cast the new promise because TypeScript cannot infer the type // when calling the "of" function through "Function.apply" return of.apply(null, waitHead.concat(waitTail)) .pipe(concatAll(), last$1(), map(() => res)); } 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); } /** * @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 */ function createEmptyUrlTree() { return new UrlTree(new UrlSegmentGroup([], {}), {}, null); } function containsTree(container, containee, exact) { if (exact) { return equalQueryParams(container.queryParams, containee.queryParams) && equalSegmentGroups(container.root, containee.root); } return containsQueryParams(container.queryParams, containee.queryParams) && containsSegmentGroup(container.root, containee.root); } function equalQueryParams(container, containee) { // TODO: This does not handle array params correctly. return shallowEqual(container, containee); } function equalSegmentGroups(container, containee) { if (!equalPath(container.segments, containee.segments)) 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])) return false; } return true; } function containsQueryParams(container, containee) { // TODO: This does not handle array params correctly. return Object.keys(containee).length <= Object.keys(container).length && Object.keys(containee).every(key => equalArraysOrString(container[key], containee[key])); } function containsSegmentGroup(container, containee) { return containsSegmentGroupHelper(container, containee, containee.segments); } function containsSegmentGroupHelper(container, containee, containeePaths) { 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; return true; } else if (container.segments.length === containeePaths.length) { if (!equalPath(container.segments, containeePaths)) return false; for (const c in containee.children) { if (!container.children[c]) return false; if (!containsSegmentGroup(container.children[c], containee.children[c])) 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 (!container.children[PRIMARY_OUTLET]) return false; return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next); } } /** * @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 { /** @internal */ constructor( /** The root segment group of the URL tree */ root, /** The query params of the URL */ queryParams, /** The fragment of the URL */ fragment) { this.root = root; this.queryParams = queryParams; this.fragment = fragment; } 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 { } /** * @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)}`]; }); 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)}`; }); 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 Error(`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 Error(`Cannot parse url '${this.url}'`); } let outletName = undefined; if (path.indexOf(':') > -1) { outletName = path.substr(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 Error(`Expected "${str}".`); } } } /** * @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 */ class Tree { constructor(root) { this._root = root; } get root() { return this._root.value; } /** * @internal */ parent(t) { const p = this.pathFromRoot(t); return p.length > 1 ? p[p.length - 2] : null; } /** * @internal */ children(t) { const n = findNode(t, this._root); return n ? n.children.map(t => t.value) : []; } /** * @internal */ firstChild(t) { const n = findNode(t, this._root); return n && n.children.length > 0 ? n.children[0].value : null; } /** * @internal */ siblings(t) { const p = findPath(t, this._root); if (p.length < 2) return []; const c = p[p.length - 2].children.map(c => c.value); return c.filter(cc => cc !== t); } /** * @internal */ pathFromRoot(t) { return findPath(t, this._root).map(s => s.value); } } // DFS for the node matching the value function findNode(value, node) { if (value === node.value) return node; for (const child of node.children) { const node = findNode(value, child); if (node) return node; } return null; } // Return the path to the node with the given value using DFS function findPath(value, node) { if (value === node.value) return [node]; for (const child of node.children) { const path = findPath(value, child); if (path.length) { path.unshift(node); return path; } } return []; } class TreeNode { constructor(value, children) { this.value = value; this.children = children; } toString() { return `TreeNode(${this.value})`; } } // Return the list of T indexed by outlet name function nodeChildrenAsMap(node) { const map = {}; if (node) { node.children.forEach(child => map[child.value.outlet] = child); } return map; } /** * @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 */ /** * Represents the state of the router as a tree of activated routes. * * @usageNotes * * Every node in the route tree is an `ActivatedRoute` instance * that knows about the "consumed" URL segments, the extracted parameters, * and the resolved data. * Use the `ActivatedRoute` properties to traverse the tree from any node. * * ### Example * * ``` * @Component({templateUrl:'template.html'}) * class MyComponent { * constructor(router: Router) { * const state: RouterState = router.routerState; * const root: ActivatedRoute = state.root; * const child = root.firstChild; * const id: Observable<string> = child.params.map(p => p.id); * //... * } * } * ``` * * @see `ActivatedRoute` * * @publicApi */ class RouterState extends Tree { /** @internal */ constructor(root, /** The current snapshot of the router state */ snapshot) { super(root); this.snapshot = snapshot; setRouterState(this, root); } toString() { return this.snapshot.toString(); } } function createEmptyState(urlTree, rootComponent) { const snapshot = createEmptyStateSnapshot(urlTree, rootComponent); const emptyUrl = new BehaviorSubject([new UrlSegment('', {})]); const emptyParams = new BehaviorSubject({}); const emptyData = new BehaviorSubject({}); const emptyQueryParams = new BehaviorSubject({}); const fragment = new BehaviorSubject(''); const activated = new ActivatedRoute(emptyUrl, emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, snapshot.root); activated.snapshot = snapshot.root; return new RouterState(new TreeNode(activated, []), snapshot); } function createEmptyStateSnapshot(urlTree, rootComponent) { const emptyParams = {}; const emptyData = {}; const emptyQueryParams = {}; const fragment = ''; const activated = new ActivatedRouteSnapshot([], emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, null, urlTree.root, -1, {}); return new RouterStateSnapshot('', new TreeNode(activated, [])); } /** * Provides access to information about a route associated with a component * that is loaded in an outlet. * Use to traverse the `RouterState` tree and extract information from nodes. * * {@example router/activated-route/module.ts region="activated-route" * header="activated-route.component.ts"} * * @publicApi */ class ActivatedRoute { /** @internal */ constructor( /** An observable of the URL segments matched by this route. */ url, /** An observable of the matrix parameters scoped to this route. */ params, /** An observable of the query parameters shared by all the routes. */ queryParams, /** An observable of the URL fragment shared by all the routes. */ fragment, /** An observable of the static and resolved data of this route. */ data, /** The outlet name of the route, a constant. */ outlet, /** The component of the route, a constant. */ // TODO(vsavkin): remove |string component, futureSnapshot) { this.url = url; this.params = params; this.queryParams = queryParams; this.fragment = fragment; this.data = data; this.outlet = outlet; this.component = component; this._futureSnapshot = futureSnapshot; } /** The configuration used to match this route. */ get routeConfig() { return this._futureSnapshot.routeConfig; } /** The root of the router state. */ get root() { return this._routerState.root; } /** The parent of this route in the router state tree. */ get parent() { return this._routerState.parent(this); } /** The first child of this route in the router state tree. */ get firstChild() { return this._routerState.firstChild(this); } /** The children of this route in the router state tree. */ get children() { return this._routerState.children(this); } /** The path from the root of the router state tree to this route. */ get pathFromRoot() { return this._routerState.pathFromRoot(this); } /** * An Observable that contains a map of the required and optional parameters * specific to the route. * The map supports retrieving single and multiple values from the same parameter. */ get paramMap() { if (!this._paramMap) { this._paramMap = this.params.pipe(map((p) => convertToParamMap(p))); } return this._paramMap; } /** * An Observable that contains a map of the query parameters available to all routes. * The map supports retrieving single and multiple values from the query parameter. */ get queryParamMap() { if (!this._queryParamMap) { this._queryParamMap = this.queryParams.pipe(map((p) => convertToParamMap(p))); } return this._queryParamMap; } toString() { return this.snapshot ? this.snapshot.toString() : `Future(${this._futureSnapshot})`; } } /** * Returns the inherited params, data, and resolve for a given route. * By default, this only inherits values up to the nearest path-less or component-less route. * @internal */ function inheritedParamsDataResolve(route, paramsInheritanceStrategy = 'emptyOnly') { const pathFromRoot = route.pathFromRoot; let inheritingStartingFrom = 0; if (paramsInheritanceStrategy !== 'always') { inheritingStartingFrom = pathFromRoot.length - 1; while (inheritingStartingFrom >= 1) { const current = pathFromRoot[inheritingStartingFrom]; const parent = pathFromRoot[inheritingStartingFrom - 1]; // current route is an empty path => inherits its parent's params and data if (current.routeConfig && current.routeConfig.path === '') { inheritingStartingFrom--; // parent is componentless => current route should inherit its params and data } else if (!parent.component) { inheritingStartingFrom--; } else { break; } } } return flattenInherited(pathFromRoot.slice(inheritingStartingFrom)); } /** @internal */ function flattenInherited(pathFromRoot) { return pathFromRoot.reduce((res, curr) => { const params = Object.assign(Object.assign({}, res.params), curr.params); const data = Object.assign(Object.assign({}, res.data), curr.data); const resolve = Object.assign(Object.assign({}, res.resolve), curr._resolvedData); return { params, data, resolve }; }, { params: {}, data: {}, resolve: {} }); } /** * @description * * Contains the information about a route associated with a component loaded in an * outlet at a particular moment in time. ActivatedRouteSnapshot can also be used to * traverse the router state tree. * * ``` * @Component({templateUrl:'./my-component.html'}) * class MyComponent { * constructor(route: ActivatedRoute) { * const id: string = route.snapshot.params.id; * const url: string = route.snapshot.url.join(''); * const user = route.snapshot.data.user; * } * } * ``` * * @publicApi */ class ActivatedRouteSnapshot { /** @internal */ constructor( /** The URL segments matched by this route */ url, /** The matrix parameters scoped to this route */ params, /** The query parameters shared by all the routes */ queryParams, /** The URL fragment shared by all the routes */ fragment, /** The static and resolved data of this route */ data, /** The outlet name of the route */ outlet, /** The component of the route */ component, routeConfig, urlSegment, lastPathInd