Skip to content

Commit 6c05c80

Browse files
atscottzarend
authored andcommitted
feat(router): Add more find-tuned control in routerLinkActiveOptions (#40303)
This commit adds more configurability to the `Router#isActive` method and `RouterLinkActive#routerLinkActiveOptions`. It allows tuning individual match options for query params and the url tree, which were either both partial or both exact matches in the past. Additionally, it also allows matching against the fragment and matrix parameters. fixes #13205 BREAKING CHANGE: The type of the `RouterLinkActive.routerLinkActiveOptions` input was expanded to allow more fine-tuned control. Code that previously read this property may need to be updated to account for the new type. PR Close #40303
1 parent 29d8a0a commit 6c05c80

File tree

8 files changed

+324
-58
lines changed

8 files changed

+324
-58
lines changed

goldens/public-api/router/router.d.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ export declare class GuardsCheckStart extends RouterEvent {
158158

159159
export declare type InitialNavigation = 'disabled' | 'enabled' | 'enabledBlocking' | 'enabledNonBlocking';
160160

161+
export declare interface IsActiveMatchOptions {
162+
fragment: 'exact' | 'ignored';
163+
matrixParams: 'exact' | 'subset' | 'ignored';
164+
paths: 'exact' | 'subset';
165+
queryParams: 'exact' | 'subset' | 'ignored';
166+
}
167+
161168
export declare type LoadChildren = LoadChildrenCallback | DeprecatedLoadChildren;
162169

163170
export declare type LoadChildrenCallback = () => Type<any> | NgModuleFactory<any> | Observable<Type<any>> | Promise<NgModuleFactory<any> | Type<any> | any>;
@@ -345,7 +352,8 @@ export declare class Router {
345352
dispose(): void;
346353
getCurrentNavigation(): Navigation | null;
347354
initialNavigation(): void;
348-
isActive(url: string | UrlTree, exact: boolean): boolean;
355+
/** @deprecated */ isActive(url: string | UrlTree, exact: boolean): boolean;
356+
isActive(url: string | UrlTree, matchOptions: IsActiveMatchOptions): boolean;
349357
navigate(commands: any[], extras?: NavigationExtras): Promise<boolean>;
350358
navigateByUrl(url: string | UrlTree, extras?: NavigationBehaviorOptions): Promise<boolean>;
351359
ngOnDestroy(): void;
@@ -400,7 +408,7 @@ export declare class RouterLinkActive implements OnChanges, OnDestroy, AfterCont
400408
set routerLinkActive(data: string[] | string);
401409
routerLinkActiveOptions: {
402410
exact: boolean;
403-
};
411+
} | IsActiveMatchOptions;
404412
constructor(router: Router, element: ElementRef, renderer: Renderer2, cdr: ChangeDetectorRef, link?: RouterLink | undefined, linkWithHref?: RouterLinkWithHref | undefined);
405413
ngAfterContentInit(): void;
406414
ngOnChanges(changes: SimpleChanges): void;

goldens/size-tracking/integration-payloads.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"master": {
4040
"uncompressed": {
4141
"runtime-es2015": 2285,
42-
"main-es2015": 241843,
42+
"main-es2015": 242531,
4343
"polyfills-es2015": 36709,
4444
"5-es2015": 745
4545
}
@@ -49,7 +49,7 @@
4949
"master": {
5050
"uncompressed": {
5151
"runtime-es2015": 2289,
52-
"main-es2015": 217591,
52+
"main-es2015": 218317,
5353
"polyfills-es2015": 36723,
5454
"5-es2015": 781
5555
}

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,9 @@
11991199
{
12001200
"name": "equalPath"
12011201
},
1202+
{
1203+
"name": "exactMatchOptions"
1204+
},
12021205
{
12031206
"name": "executeCheckHooks"
12041207
},
@@ -1661,6 +1664,9 @@
16611664
{
16621665
"name": "materializeViewResults"
16631666
},
1667+
{
1668+
"name": "matrixParamsMatch"
1669+
},
16641670
{
16651671
"name": "maybeUnwrapFn"
16661672
},
@@ -1754,6 +1760,12 @@
17541760
{
17551761
"name": "optionsReducer"
17561762
},
1763+
{
1764+
"name": "paramCompareMap"
1765+
},
1766+
{
1767+
"name": "pathCompareMap"
1768+
},
17571769
{
17581770
"name": "pipeFromArray"
17591771
},
@@ -1949,6 +1961,9 @@
19491961
{
19501962
"name": "subscribeToResult"
19511963
},
1964+
{
1965+
"name": "subsetMatchOptions"
1966+
},
19521967
{
19531968
"name": "supportsState"
19541969
},

packages/router/src/directives/router_link_active.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {mergeAll} from 'rxjs/operators';
1212

1313
import {Event, NavigationEnd} from '../events';
1414
import {Router} from '../router';
15+
import {IsActiveMatchOptions} from '../url_tree';
1516

1617
import {RouterLink, RouterLinkWithHref} from './router_link';
1718

@@ -89,7 +90,15 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
8990
private linkInputChangesSubscription?: Subscription;
9091
public readonly isActive: boolean = false;
9192

92-
@Input() routerLinkActiveOptions: {exact: boolean} = {exact: false};
93+
/**
94+
* Options to configure how to determine if the router link is active.
95+
*
96+
* These options are passed to the `Router.isActive()` function.
97+
*
98+
* @see Router.isActive
99+
*/
100+
@Input() routerLinkActiveOptions: {exact: boolean}|IsActiveMatchOptions = {exact: false};
101+
93102

94103
constructor(
95104
private router: Router, private element: ElementRef, private renderer: Renderer2,
@@ -159,8 +168,11 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
159168
}
160169

161170
private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean {
162-
return (link: RouterLink|RouterLinkWithHref) =>
163-
router.isActive(link.urlTree, this.routerLinkActiveOptions.exact);
171+
const options = 'paths' in this.routerLinkActiveOptions ?
172+
this.routerLinkActiveOptions :
173+
// While the types should disallow `undefined` here, it's possible without strict inputs
174+
(this.routerLinkActiveOptions.exact || false);
175+
return (link: RouterLink|RouterLinkWithHref) => router.isActive(link.urlTree, options);
164176
}
165177

166178
private hasActiveLinks(): boolean {
@@ -169,4 +181,4 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
169181
this.linkWithHref && isActiveCheckFn(this.linkWithHref) ||
170182
this.links.some(isActiveCheckFn) || this.linksWithHrefs.some(isActiveCheckFn);
171183
}
172-
}
184+
}

packages/router/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} fr
2222
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
2323
export {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET} from './shared';
2424
export {UrlHandlingStrategy} from './url_handling_strategy';
25-
export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
25+
export {DefaultUrlSerializer, IsActiveMatchOptions, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
2626
export {VERSION} from './version';
2727

2828
export * from './private_export';

packages/router/src/router.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {ChildrenOutletContexts} from './router_outlet_context';
2727
import {ActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from './router_state';
2828
import {isNavigationCancelingError, navigationCancelingError, Params} from './shared';
2929
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
30-
import {containsTree, createEmptyUrlTree, UrlSerializer, UrlTree} from './url_tree';
30+
import {containsTree, createEmptyUrlTree, IsActiveMatchOptions, UrlSerializer, UrlTree} from './url_tree';
3131
import {standardizeConfig, validateConfig} from './utils/config';
3232
import {Checks, getAllRouteGuards} from './utils/preactivation';
3333
import {isUrlTree} from './utils/type_guards';
@@ -356,6 +356,29 @@ type LocationChangeInfo = {
356356
transitionId: number
357357
};
358358

359+
/**
360+
* The equivalent `IsActiveUrlTreeOptions` options for `Router.isActive` is called with `false`
361+
* (exact = true).
362+
*/
363+
export const exactMatchOptions: IsActiveMatchOptions = {
364+
paths: 'exact',
365+
fragment: 'ignored',
366+
matrixParams: 'ignored',
367+
queryParams: 'exact'
368+
};
369+
370+
/**
371+
* The equivalent `IsActiveUrlTreeOptions` options for `Router.isActive` is called with `false`
372+
* (exact = false).
373+
*/
374+
export const subsetMatchOptions: IsActiveMatchOptions = {
375+
paths: 'subset',
376+
fragment: 'ignored',
377+
matrixParams: 'ignored',
378+
queryParams: 'subset'
379+
};
380+
381+
359382
/**
360383
* @description
361384
*
@@ -1213,14 +1236,39 @@ export class Router {
12131236
return urlTree;
12141237
}
12151238

1216-
/** Returns whether the url is activated */
1217-
isActive(url: string|UrlTree, exact: boolean): boolean {
1239+
/**
1240+
* Returns whether the url is activated.
1241+
*
1242+
* @deprecated
1243+
* Use `IsActiveUrlTreeOptions` instead.
1244+
*
1245+
* - The equivalent `IsActiveUrlTreeOptions` for `true` is
1246+
* `{paths: 'exact', queryParams: 'exact', fragment: 'ignored', matrixParams: 'ignored'}`.
1247+
* - The equivalent for `false` is
1248+
* `{paths: 'subset', queryParams: 'subset', fragment: 'ignored', matrixParams: 'ignored'}`.
1249+
*/
1250+
isActive(url: string|UrlTree, exact: boolean): boolean;
1251+
/**
1252+
* Returns whether the url is activated.
1253+
*/
1254+
isActive(url: string|UrlTree, matchOptions: IsActiveMatchOptions): boolean;
1255+
/** @internal */
1256+
isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean;
1257+
isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean {
1258+
let options: IsActiveMatchOptions;
1259+
if (matchOptions === true) {
1260+
options = {...exactMatchOptions};
1261+
} else if (matchOptions === false) {
1262+
options = {...subsetMatchOptions};
1263+
} else {
1264+
options = matchOptions;
1265+
}
12181266
if (isUrlTree(url)) {
1219-
return containsTree(this.currentUrlTree, url, exact);
1267+
return containsTree(this.currentUrlTree, url, options);
12201268
}
12211269

12221270
const urlTree = this.parseUrl(url);
1223-
return containsTree(this.currentUrlTree, urlTree, exact);
1271+
return containsTree(this.currentUrlTree, urlTree, options);
12241272
}
12251273

12261274
private removeEmptyProps(params: Params): Params {

packages/router/src/url_tree.ts

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,149 @@ export function createEmptyUrlTree() {
1313
return new UrlTree(new UrlSegmentGroup([], {}), {}, null);
1414
}
1515

16-
export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean {
17-
if (exact) {
18-
return equalQueryParams(container.queryParams, containee.queryParams) &&
19-
equalSegmentGroups(container.root, containee.root);
20-
}
16+
/**
17+
* A set of options which specify how to determine if a `UrlTree` is active, given the `UrlTree`
18+
* for the current router state.
19+
*
20+
* @publicApi
21+
* @see Router.isActive
22+
*/
23+
export interface IsActiveMatchOptions {
24+
/**
25+
* Defines the strategy for comparing the matrix parameters of two `UrlTree`s.
26+
*
27+
* The matrix parameter matching is dependent on the strategy for matching the
28+
* segments. That is, if the `paths` option is set to `'subset'`, only
29+
* the matrix parameters of the matching segments will be compared.
30+
*
31+
* - `'exact'`: Requires that matching segments also have exact matrix parameter
32+
* matches.
33+
* - `'subset'`: The matching segments in the router's active `UrlTree` may contain
34+
* extra matrix parameters, but those that exist in the `UrlTree` in question must match.
35+
* - `'ignored'`: When comparing `UrlTree`s, matrix params will be ignored.
36+
*/
37+
matrixParams: 'exact'|'subset'|'ignored';
38+
/**
39+
* Defines the strategy for comparing the query parameters of two `UrlTree`s.
40+
*
41+
* - `'exact'`: the query parameters must match exactly.
42+
* - `'subset'`: the active `UrlTree` may contain extra parameters,
43+
* but must match the key and value of any that exist in the `UrlTree` in question.
44+
* - `'ignored'`: When comparing `UrlTree`s, query params will be ignored.
45+
*/
46+
queryParams: 'exact'|'subset'|'ignored';
47+
/**
48+
* Defines the strategy for comparing the `UrlSegment`s of the `UrlTree`s.
49+
*
50+
* - `'exact'`: all segments in each `UrlTree` must match.
51+
* - `'subset'`: a `UrlTree` will be determined to be active if it
52+
* is a subtree of the active route. That is, the active route may contain extra
53+
* segments, but must at least have all the segements of the `UrlTree` in question.
54+
*/
55+
paths: 'exact'|'subset';
56+
/**
57+
* - 'exact'`: indicates that the `UrlTree` fragments must be equal.
58+
* - `'ignored'`: the fragments will not be compared when determining if a
59+
* `UrlTree` is active.
60+
*/
61+
fragment: 'exact'|'ignored';
62+
}
2163

22-
return containsQueryParams(container.queryParams, containee.queryParams) &&
23-
containsSegmentGroup(container.root, containee.root);
64+
type ParamMatchOptions = 'exact'|'subset'|'ignored';
65+
66+
type PathCompareFn =
67+
(container: UrlSegmentGroup, containee: UrlSegmentGroup, matrixParams: ParamMatchOptions) =>
68+
boolean;
69+
type ParamCompareFn = (container: Params, containee: Params) => boolean;
70+
71+
const pathCompareMap: Record<IsActiveMatchOptions['paths'], PathCompareFn> = {
72+
'exact': equalSegmentGroups,
73+
'subset': containsSegmentGroup,
74+
};
75+
const paramCompareMap: Record<ParamMatchOptions, ParamCompareFn> = {
76+
'exact': equalParams,
77+
'subset': containsParams,
78+
'ignored': () => true,
79+
};
80+
81+
export function containsTree(
82+
container: UrlTree, containee: UrlTree, options: IsActiveMatchOptions): boolean {
83+
return pathCompareMap[options.paths](container.root, containee.root, options.matrixParams) &&
84+
paramCompareMap[options.queryParams](container.queryParams, containee.queryParams) &&
85+
!(options.fragment === 'exact' && container.fragment !== containee.fragment);
2486
}
2587

26-
function equalQueryParams(container: Params, containee: Params): boolean {
88+
function equalParams(container: Params, containee: Params): boolean {
2789
// TODO: This does not handle array params correctly.
2890
return shallowEqual(container, containee);
2991
}
3092

31-
function equalSegmentGroups(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
93+
function equalSegmentGroups(
94+
container: UrlSegmentGroup, containee: UrlSegmentGroup,
95+
matrixParams: ParamMatchOptions): boolean {
3296
if (!equalPath(container.segments, containee.segments)) return false;
97+
if (!matrixParamsMatch(container.segments, containee.segments, matrixParams)) {
98+
return false;
99+
}
33100
if (container.numberOfChildren !== containee.numberOfChildren) return false;
34101
for (const c in containee.children) {
35102
if (!container.children[c]) return false;
36-
if (!equalSegmentGroups(container.children[c], containee.children[c])) return false;
103+
if (!equalSegmentGroups(container.children[c], containee.children[c], matrixParams))
104+
return false;
37105
}
38106
return true;
39107
}
40108

41-
function containsQueryParams(container: Params, containee: Params): boolean {
109+
function containsParams(container: Params, containee: Params): boolean {
42110
return Object.keys(containee).length <= Object.keys(container).length &&
43111
Object.keys(containee).every(key => equalArraysOrString(container[key], containee[key]));
44112
}
45113

46-
function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
47-
return containsSegmentGroupHelper(container, containee, containee.segments);
114+
function containsSegmentGroup(
115+
container: UrlSegmentGroup, containee: UrlSegmentGroup,
116+
matrixParams: ParamMatchOptions): boolean {
117+
return containsSegmentGroupHelper(container, containee, containee.segments, matrixParams);
48118
}
49119

50120
function containsSegmentGroupHelper(
51-
container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[]): boolean {
121+
container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[],
122+
matrixParams: ParamMatchOptions): boolean {
52123
if (container.segments.length > containeePaths.length) {
53124
const current = container.segments.slice(0, containeePaths.length);
54125
if (!equalPath(current, containeePaths)) return false;
55126
if (containee.hasChildren()) return false;
127+
if (!matrixParamsMatch(current, containeePaths, matrixParams)) return false;
56128
return true;
57129

58130
} else if (container.segments.length === containeePaths.length) {
59131
if (!equalPath(container.segments, containeePaths)) return false;
132+
if (!matrixParamsMatch(container.segments, containeePaths, matrixParams)) return false;
60133
for (const c in containee.children) {
61134
if (!container.children[c]) return false;
62-
if (!containsSegmentGroup(container.children[c], containee.children[c])) return false;
135+
if (!containsSegmentGroup(container.children[c], containee.children[c], matrixParams)) {
136+
return false;
137+
}
63138
}
64139
return true;
65140

66141
} else {
67142
const current = containeePaths.slice(0, container.segments.length);
68143
const next = containeePaths.slice(container.segments.length);
69144
if (!equalPath(container.segments, current)) return false;
145+
if (!matrixParamsMatch(container.segments, current, matrixParams)) return false;
70146
if (!container.children[PRIMARY_OUTLET]) return false;
71-
return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next);
147+
return containsSegmentGroupHelper(
148+
container.children[PRIMARY_OUTLET], containee, next, matrixParams);
72149
}
73150
}
74151

152+
function matrixParamsMatch(
153+
containerPaths: UrlSegment[], containeePaths: UrlSegment[], options: ParamMatchOptions) {
154+
return containeePaths.every((containeeSegment, i) => {
155+
return paramCompareMap[options](containerPaths[i].parameters, containeeSegment.parameters);
156+
});
157+
}
158+
75159
/**
76160
* @description
77161
*

0 commit comments

Comments
 (0)