Skip to content

Commit

Permalink
feat(router): expose resolved route title (#46826)
Browse files Browse the repository at this point in the history
Expose resolved route title from ActivatedRoute and ActivatedRouteSnapshot

PR Close #46826
  • Loading branch information
EmmanuelRoux authored and Pawel Kozlowski committed Jul 21, 2022
1 parent 76790a6 commit 10289f1
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 16 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/router/index.md
Expand Up @@ -50,6 +50,7 @@ export class ActivatedRoute {
get root(): ActivatedRoute;
get routeConfig(): Route | null;
snapshot: ActivatedRouteSnapshot;
readonly title: Observable<string | undefined>;
// (undocumented)
toString(): string;
url: Observable<UrlSegment[]>;
Expand All @@ -73,6 +74,7 @@ export class ActivatedRouteSnapshot {
queryParams: Params;
get root(): ActivatedRouteSnapshot;
readonly routeConfig: Route | null;
readonly title?: string;
// (undocumented)
toString(): string;
url: UrlSegment[];
Expand Down
Expand Up @@ -531,7 +531,7 @@
"name": "RouteExampleModule"
},
{
"name": "RouteTitle"
"name": "RouteTitleKey"
},
{
"name": "Router"
Expand Down
12 changes: 3 additions & 9 deletions packages/router/src/operators/resolve_data.ts
Expand Up @@ -13,16 +13,10 @@ import {catchError, concatMap, first, map, mapTo, mergeMap, takeLast, tap} from
import {ResolveData, Route} from '../models';
import {NavigationTransition} from '../router';
import {ActivatedRouteSnapshot, inheritedParamsDataResolve, RouterStateSnapshot} from '../router_state';
import {RouteTitleKey} from '../shared';
import {wrapIntoObservable} from '../utils/collection';
import {getToken} from '../utils/preactivation';

/**
* 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.
*/
export const RouteTitle = Symbol('RouteTitle');

export function resolveData(
paramsInheritanceStrategy: 'emptyOnly'|'always',
moduleInjector: Injector): MonoTypeOperatorFunction<NavigationTransition> {
Expand Down Expand Up @@ -51,14 +45,14 @@ function runResolve(
const config = futureARS.routeConfig;
const resolve = futureARS._resolve;
if (config?.title !== undefined && !hasStaticTitle(config)) {
resolve[RouteTitle] = config.title;
resolve[RouteTitleKey] = config.title;
}
return resolveNode(resolve, futureARS, futureRSS, moduleInjector)
.pipe(map((resolvedData: any) => {
futureARS._resolvedData = resolvedData;
futureARS.data = inheritedParamsDataResolve(futureARS, paramsInheritanceStrategy).resolve;
if (config && hasStaticTitle(config)) {
futureARS.data[RouteTitle] = config.title;
futureARS.data[RouteTitleKey] = config.title;
}
return null;
}));
Expand Down
5 changes: 2 additions & 3 deletions packages/router/src/page_title_strategy.ts
Expand Up @@ -9,9 +9,8 @@
import {inject, Injectable} from '@angular/core';
import {Title} from '@angular/platform-browser';

import {RouteTitle as TitleKey} from './operators/resolve_data';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
import {PRIMARY_OUTLET} from './shared';
import {PRIMARY_OUTLET, RouteTitleKey} from './shared';

/**
* Provides a strategy for setting the page title after a router navigation.
Expand Down Expand Up @@ -59,7 +58,7 @@ export abstract class TitleStrategy {
* `Route.title` property, which can either be a static string or a resolved value.
*/
getResolvedTitleForRoute(snapshot: ActivatedRouteSnapshot) {
return snapshot.data[TitleKey];
return snapshot.data[RouteTitleKey];
}
}

Expand Down
11 changes: 9 additions & 2 deletions packages/router/src/router_state.ts
Expand Up @@ -7,11 +7,11 @@
*/

import {Type} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {map} from 'rxjs/operators';

import {Data, ResolveData, Route} from './models';
import {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET} from './shared';
import {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET, RouteTitleKey} from './shared';
import {equalSegments, UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {shallowEqual, shallowEqualArrays} from './utils/collection';
import {Tree, TreeNode} from './utils/tree';
Expand Down Expand Up @@ -119,6 +119,10 @@ export class ActivatedRoute {
/** @internal */
_queryParamMap!: Observable<ParamMap>;

/** An Observable of the resolved route title */
readonly title: Observable<string|undefined> =
this.data?.pipe(map((d: Data) => d[RouteTitleKey])) ?? of(undefined);

/** @internal */
constructor(
/** An observable of the URL segments matched by this route. */
Expand Down Expand Up @@ -303,6 +307,9 @@ export class ActivatedRouteSnapshot {
// TODO(issue/24571): remove '!'.
_queryParamMap!: ParamMap;

/** The resolved route title */
readonly title?: string = this.data?.[RouteTitleKey];

/** @internal */
constructor(
/** The URL segments matched by this route */
Expand Down
7 changes: 7 additions & 0 deletions packages/router/src/shared.ts
Expand Up @@ -17,6 +17,13 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
*/
export 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.
*/
export const RouteTitleKey = Symbol('RouteTitle');

/**
* A collection of matrix and query URL parameters.
* @see `convertToParamMap()`
Expand Down
22 changes: 21 additions & 1 deletion packages/router/test/router_state.spec.ts
Expand Up @@ -9,7 +9,7 @@
import {BehaviorSubject} from 'rxjs';

import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute, equalParamsAndUrlSegments, RouterState, RouterStateSnapshot} from '../src/router_state';
import {Params} from '../src/shared';
import {Params, RouteTitleKey} from '../src/shared';
import {UrlSegment} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';

Expand Down Expand Up @@ -202,6 +202,26 @@ describe('RouterState & Snapshot', () => {
expect(hasSeenDataChange).toEqual(true);
});
});

describe('ActivatedRoute', () => {
it('should get resolved route title', () => {
const data = {[RouteTitleKey]: 'resolved title'};
const route = createActivatedRoute('a');
const snapshot = new (ActivatedRouteSnapshot as any)(
<any>[], <any>null, <any>null, <any>null, data, <any>null, 'test', <any>null, <any>null,
-1, null!);
let resolvedTitle: string|undefined;

route.data.next(data);

route.title.forEach((title: string|undefined) => {
resolvedTitle = title;
});

expect(resolvedTitle).toEqual('resolved title');
expect(snapshot.title).toEqual('resolved title');
});
});
});

function createActivatedRouteSnapshot(cmp: string) {
Expand Down

0 comments on commit 10289f1

Please sign in to comment.