Raw File
router_state.ts
/**
 * @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 {Type} from '@angular/core';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {map} from 'rxjs/operators';

import {Data, ResolveData, Route} from './models';
import {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET, RouteTitleKey} from './shared';
import {equalSegments, UrlSegment, UrlTree} from './url_tree';
import {shallowEqual, shallowEqualArrays} from './utils/collection';
import {Tree, TreeNode} from './utils/tree';

/**
 * 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.
 *
 * The following fragment shows how a component gets the root node
 * of the current state to establish its own route tree:
 *
 * ```
 * @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 {@link ActivatedRoute}
 * @see [Getting route information](guide/router#getting-route-information)
 *
 * @publicApi
 */
export class RouterState extends Tree<ActivatedRoute> {
  /** @internal */
  constructor(
      root: TreeNode<ActivatedRoute>,
      /** The current snapshot of the router state */
      public snapshot: RouterStateSnapshot) {
    super(root);
    setRouterState(<RouterState>this, root);
  }

  override toString(): string {
    return this.snapshot.toString();
  }
}

export function createEmptyState(urlTree: UrlTree, rootComponent: Type<any>|null): RouterState {
  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<string|null>('');
  const activated = new ActivatedRoute(
      emptyUrl, emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent,
      snapshot.root);
  activated.snapshot = snapshot.root;
  return new RouterState(new TreeNode<ActivatedRoute>(activated, []), snapshot);
}

export function createEmptyStateSnapshot(
    urlTree: UrlTree, rootComponent: Type<any>|null): RouterStateSnapshot {
  const emptyParams = {};
  const emptyData = {};
  const emptyQueryParams = {};
  const fragment = '';
  const activated = new ActivatedRouteSnapshot(
      [], emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, null,
      {});
  return new RouterStateSnapshot('', new TreeNode<ActivatedRouteSnapshot>(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.
 *
 * The following example shows how to construct a component using information from a
 * currently activated route.
 *
 * Note: the observables in this class only emit when the current and previous values differ based
 * on shallow equality. For example, changing deeply nested properties in resolved `data` will not
 * cause the `ActivatedRoute.data` `Observable` to emit a new value.
 *
 * {@example router/activated-route/module.ts region="activated-route"
 *     header="activated-route.component.ts"}
 *
 * @see [Getting route information](guide/router#getting-route-information)
 *
 * @publicApi
 */
export class ActivatedRoute {
  /** The current snapshot of this route */
  snapshot!: ActivatedRouteSnapshot;
  /** @internal */
  _futureSnapshot: ActivatedRouteSnapshot;
  /** @internal */
  _routerState!: RouterState;
  /** @internal */
  _paramMap?: Observable<ParamMap>;
  /** @internal */
  _queryParamMap?: Observable<ParamMap>;

  /** An Observable of the resolved route title */
  readonly title: Observable<string|undefined>;

  /** An observable of the URL segments matched by this route. */
  public url: Observable<UrlSegment[]>;
  /** An observable of the matrix parameters scoped to this route. */
  public params: Observable<Params>;
  /** An observable of the query parameters shared by all the routes. */
  public queryParams: Observable<Params>;
  /** An observable of the URL fragment shared by all the routes. */
  public fragment: Observable<string|null>;
  /** An observable of the static and resolved data of this route. */
  public data: Observable<Data>;

  /** @internal */
  constructor(
      /** @internal */
      public urlSubject: BehaviorSubject<UrlSegment[]>,
      /** @internal */
      public paramsSubject: BehaviorSubject<Params>,
      /** @internal */
      public queryParamsSubject: BehaviorSubject<Params>,
      /** @internal */
      public fragmentSubject: BehaviorSubject<string|null>,
      /** @internal */
      public dataSubject: BehaviorSubject<Data>,
      /** The outlet name of the route, a constant. */
      public outlet: string,
      /** The component of the route, a constant. */
      public component: Type<any>|null, futureSnapshot: ActivatedRouteSnapshot) {
    this._futureSnapshot = futureSnapshot;
    this.title = this.dataSubject?.pipe(map((d: Data) => d[RouteTitleKey])) ?? of(undefined);
    // TODO(atscott): Verify that these can be changed to `.asObservable()` with TGP.
    this.url = urlSubject;
    this.params = paramsSubject;
    this.queryParams = queryParamsSubject;
    this.fragment = fragmentSubject;
    this.data = dataSubject;
  }

  /** The configuration used to match this route. */
  get routeConfig(): Route|null {
    return this._futureSnapshot.routeConfig;
  }

  /** The root of the router state. */
  get root(): ActivatedRoute {
    return this._routerState.root;
  }

  /** The parent of this route in the router state tree. */
  get parent(): ActivatedRoute|null {
    return this._routerState.parent(this);
  }

  /** The first child of this route in the router state tree. */
  get firstChild(): ActivatedRoute|null {
    return this._routerState.firstChild(this);
  }

  /** The children of this route in the router state tree. */
  get children(): ActivatedRoute[] {
    return this._routerState.children(this);
  }

  /** The path from the root of the router state tree to this route. */
  get pathFromRoot(): ActivatedRoute[] {
    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(): Observable<ParamMap> {
    if (!this._paramMap) {
      this._paramMap = this.params.pipe(map((p: Params): ParamMap => 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(): Observable<ParamMap> {
    if (!this._queryParamMap) {
      this._queryParamMap =
          this.queryParams.pipe(map((p: Params): ParamMap => convertToParamMap(p)));
    }
    return this._queryParamMap;
  }

  toString(): string {
    return this.snapshot ? this.snapshot.toString() : `Future(${this._futureSnapshot})`;
  }
}

export type ParamsInheritanceStrategy = 'emptyOnly'|'always';

/** @internal */
export type Inherited = {
  params: Params,
  data: Data,
  resolve: Data,
};

/**
 * Returns the inherited params, data, and resolve for a given route.
 *
 * By default, we do not inherit parent data unless the current route is path-less or the parent
 * route is component-less.
 */
export function getInherited(
    route: ActivatedRouteSnapshot, parent: ActivatedRouteSnapshot|null,
    paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly'): Inherited {
  let inherited: Inherited;
  const {routeConfig} = route;
  if (parent !== null &&
      (paramsInheritanceStrategy === 'always' ||
       // inherit parent data if route is empty path
       routeConfig?.path === '' ||
       // inherit parent data if parent was componentless
       (!parent.component && !parent.routeConfig?.loadComponent))) {
    inherited = {
      params: {...parent.params, ...route.params},
      data: {...parent.data, ...route.data},
      resolve: {
        // Snapshots are created with data inherited from parent and guards (i.e. canActivate) can
        // change data because it's not frozen...
        // This first line could be deleted chose to break/disallow mutating the `data` object in
        // guards.
        // Note that data from parents still override this mutated data so anyone relying on this
        // might be surprised that it doesn't work if parent data is inherited but otherwise does.
        ...route.data,
        // Ensure inherited resolved data overrides inherited static data
        ...parent.data,
        // static data from the current route overrides any inherited data
        ...routeConfig?.data,
        // resolved data from current route overrides everything
        ...route._resolvedData,
      }
    };
  } else {
    inherited = {
      params: route.params,
      data: route.data,
      resolve: {...route.data, ...(route._resolvedData ?? {})}
    };
  }

  if (routeConfig && hasStaticTitle(routeConfig)) {
    inherited.resolve[RouteTitleKey] = routeConfig.title;
  }
  return inherited;
}

/**
 * @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.
 *
 * The following example initializes a component with route information extracted
 * from the snapshot of the root node at the time of creation.
 *
 * ```
 * @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
 */
export class ActivatedRouteSnapshot {
  /** The configuration used to match this route **/
  public readonly routeConfig: Route|null;
  /** @internal */
  _resolve: ResolveData;
  /** @internal */
  _resolvedData?: Data;
  /** @internal */
  _routerState!: RouterStateSnapshot;
  /** @internal */
  _paramMap?: ParamMap;
  /** @internal */
  _queryParamMap?: ParamMap;

  /** The resolved route title */
  get title(): string|undefined {
    // Note: This _must_ be a getter because the data is mutated in the resolvers. Title will not be
    // available at the time of class instantiation.
    return this.data?.[RouteTitleKey];
  }

  /** @internal */
  constructor(
      /** The URL segments matched by this route */
      public url: UrlSegment[],
      /**
       *  The matrix parameters scoped to this route.
       *
       *  You can compute all params (or data) in the router state or to get params outside
       *  of an activated component by traversing the `RouterState` tree as in the following
       *  example:
       *  ```
       *  collectRouteParams(router: Router) {
       *    let params = {};
       *    let stack: ActivatedRouteSnapshot[] = [router.routerState.snapshot.root];
       *    while (stack.length > 0) {
       *      const route = stack.pop()!;
       *      params = {...params, ...route.params};
       *      stack.push(...route.children);
       *    }
       *    return params;
       *  }
       *  ```
       */
      public params: Params,
      /** The query parameters shared by all the routes */
      public queryParams: Params,
      /** The URL fragment shared by all the routes */
      public fragment: string|null,
      /** The static and resolved data of this route */
      public data: Data,
      /** The outlet name of the route */
      public outlet: string,
      /** The component of the route */
      public component: Type<any>|null, routeConfig: Route|null, resolve: ResolveData) {
    this.routeConfig = routeConfig;
    this._resolve = resolve;
  }

  /** The root of the router state */
  get root(): ActivatedRouteSnapshot {
    return this._routerState.root;
  }

  /** The parent of this route in the router state tree */
  get parent(): ActivatedRouteSnapshot|null {
    return this._routerState.parent(this);
  }

  /** The first child of this route in the router state tree */
  get firstChild(): ActivatedRouteSnapshot|null {
    return this._routerState.firstChild(this);
  }

  /** The children of this route in the router state tree */
  get children(): ActivatedRouteSnapshot[] {
    return this._routerState.children(this);
  }

  /** The path from the root of the router state tree to this route */
  get pathFromRoot(): ActivatedRouteSnapshot[] {
    return this._routerState.pathFromRoot(this);
  }

  get paramMap(): ParamMap {
    if (!this._paramMap) {
      this._paramMap = convertToParamMap(this.params);
    }
    return this._paramMap;
  }

  get queryParamMap(): ParamMap {
    if (!this._queryParamMap) {
      this._queryParamMap = convertToParamMap(this.queryParams);
    }
    return this._queryParamMap;
  }

  toString(): string {
    const url = this.url.map(segment => segment.toString()).join('/');
    const matched = this.routeConfig ? this.routeConfig.path : '';
    return `Route(url:'${url}', path:'${matched}')`;
  }
}

/**
 * @description
 *
 * Represents the state of the router at a moment in time.
 *
 * This is a tree of activated route snapshots. Every node in this tree knows about
 * the "consumed" URL segments, the extracted parameters, and the resolved data.
 *
 * The following example shows how a component is initialized with information
 * from the snapshot of the root node's state at the time of creation.
 *
 * ```
 * @Component({templateUrl:'template.html'})
 * class MyComponent {
 *   constructor(router: Router) {
 *     const state: RouterState = router.routerState;
 *     const snapshot: RouterStateSnapshot = state.snapshot;
 *     const root: ActivatedRouteSnapshot = snapshot.root;
 *     const child = root.firstChild;
 *     const id: Observable<string> = child.params.map(p => p.id);
 *     //...
 *   }
 * }
 * ```
 *
 * @publicApi
 */
export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
  /** @internal */
  constructor(
      /** The url from which this snapshot was created */
      public url: string, root: TreeNode<ActivatedRouteSnapshot>) {
    super(root);
    setRouterState(<RouterStateSnapshot>this, root);
  }

  override toString(): string {
    return serializeNode(this._root);
  }
}

function setRouterState<U, T extends {_routerState: U}>(state: U, node: TreeNode<T>): void {
  node.value._routerState = state;
  node.children.forEach(c => setRouterState(state, c));
}

function serializeNode(node: TreeNode<ActivatedRouteSnapshot>): string {
  const c = node.children.length > 0 ? ` { ${node.children.map(serializeNode).join(', ')} } ` : '';
  return `${node.value}${c}`;
}

/**
 * The expectation is that the activate route is created with the right set of parameters.
 * So we push new values into the observables only when they are not the initial values.
 * And we detect that by checking if the snapshot field is set.
 */
export function advanceActivatedRoute(route: ActivatedRoute): void {
  if (route.snapshot) {
    const currentSnapshot = route.snapshot;
    const nextSnapshot = route._futureSnapshot;
    route.snapshot = nextSnapshot;
    if (!shallowEqual(currentSnapshot.queryParams, nextSnapshot.queryParams)) {
      route.queryParamsSubject.next(nextSnapshot.queryParams);
    }
    if (currentSnapshot.fragment !== nextSnapshot.fragment) {
      route.fragmentSubject.next(nextSnapshot.fragment);
    }
    if (!shallowEqual(currentSnapshot.params, nextSnapshot.params)) {
      route.paramsSubject.next(nextSnapshot.params);
    }
    if (!shallowEqualArrays(currentSnapshot.url, nextSnapshot.url)) {
      route.urlSubject.next(nextSnapshot.url);
    }
    if (!shallowEqual(currentSnapshot.data, nextSnapshot.data)) {
      route.dataSubject.next(nextSnapshot.data);
    }
  } else {
    route.snapshot = route._futureSnapshot;

    // this is for resolved data
    route.dataSubject.next(route._futureSnapshot.data);
  }
}


export function equalParamsAndUrlSegments(
    a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
  const equalUrlParams = shallowEqual(a.params, b.params) && equalSegments(a.url, b.url);
  const parentsMismatch = !a.parent !== !b.parent;

  return equalUrlParams && !parentsMismatch &&
      (!a.parent || equalParamsAndUrlSegments(a.parent, b.parent!));
}

export function hasStaticTitle(config: Route) {
  return typeof config.title === 'string' || config.title === null;
}
back to top