Skip to content

Commit 907a94d

Browse files
atscottleonsenft
authored andcommitted
feat(router): Update IsActiveMatchOptions APIs to accept a Partial
This updates `RouterLinkActive`, `Router.isActive`, and the standalone `isActive` function to accept `Partial<IsActiveMatchOptions>` which uses the current default values as the base (paths and queryParams are subset, fragment and matrix params are ignored). fixes #53326
1 parent cf9620f commit 907a94d

6 files changed

Lines changed: 84 additions & 20 deletions

File tree

goldens/public-api/router/index.api.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ export interface InMemoryScrollingOptions {
370370
}
371371

372372
// @public
373-
export function isActive(url: string | UrlTree, router: Router, matchOptions?: IsActiveMatchOptions): Signal<boolean>;
373+
export function isActive(url: string | UrlTree, router: Router, matchOptions?: Partial<IsActiveMatchOptions>): Signal<boolean>;
374374

375375
// @public
376376
export interface IsActiveMatchOptions {
@@ -728,8 +728,8 @@ export class Router {
728728
initialNavigation(): void;
729729
// @deprecated
730730
isActive(url: string | UrlTree, exact: boolean): boolean;
731-
// @deprecated
732-
isActive(url: string | UrlTree, matchOptions: IsActiveMatchOptions): boolean;
731+
// @deprecated (undocumented)
732+
isActive(url: string | UrlTree, matchOptions: Partial<IsActiveMatchOptions>): boolean;
733733
get lastSuccessfulNavigation(): Signal<Navigation | null>;
734734
navigate(commands: readonly any[], extras?: NavigationExtras): Promise<boolean>;
735735
navigateByUrl(url: string | UrlTree, extras?: NavigationBehaviorOptions): Promise<boolean>;
@@ -885,7 +885,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
885885
set routerLinkActive(data: string[] | string);
886886
routerLinkActiveOptions: {
887887
exact: boolean;
888-
} | IsActiveMatchOptions;
888+
} | Partial<IsActiveMatchOptions>;
889889
// (undocumented)
890890
static ɵdir: i0.ɵɵDirectiveDeclaration<RouterLinkActive, "[routerLinkActive]", ["routerLinkActive"], { "routerLinkActiveOptions": { "alias": "routerLinkActiveOptions"; "required": false; }; "ariaCurrentWhenActive": { "alias": "ariaCurrentWhenActive"; "required": false; }; "routerLinkActive": { "alias": "routerLinkActive"; "required": false; }; }, { "isActiveChange": "isActiveChange"; }, ["links"], never, true, never>;
891891
// (undocumented)

packages/router/src/directives/router_link_active.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
129129
*
130130
* @see {@link isActive}
131131
*/
132-
@Input() routerLinkActiveOptions: {exact: boolean} | IsActiveMatchOptions = {exact: false};
132+
@Input() routerLinkActiveOptions: {exact: boolean} | Partial<IsActiveMatchOptions> = {
133+
exact: false,
134+
};
133135

134136
/**
135137
* Aria-current attribute to apply when the router link is active.
@@ -247,7 +249,9 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
247249
}
248250

249251
private isLinkActive(router: Router): (link: RouterLink) => boolean {
250-
const options: IsActiveMatchOptions = isActiveMatchOptions(this.routerLinkActiveOptions)
252+
const options: Partial<IsActiveMatchOptions> = isActiveMatchOptions(
253+
this.routerLinkActiveOptions,
254+
)
251255
? this.routerLinkActiveOptions
252256
: // While the types should disallow `undefined` here, it's possible without strict inputs
253257
(this.routerLinkActiveOptions.exact ?? false)
@@ -270,7 +274,8 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
270274
* Use instead of `'paths' in options` to be compatible with property renaming
271275
*/
272276
function isActiveMatchOptions(
273-
options: {exact: boolean} | IsActiveMatchOptions,
274-
): options is IsActiveMatchOptions {
275-
return !!(options as IsActiveMatchOptions).paths;
277+
options: {exact: boolean} | Partial<IsActiveMatchOptions>,
278+
): options is Partial<IsActiveMatchOptions> {
279+
const o = options as Partial<IsActiveMatchOptions>;
280+
return !!(o.paths || o.matrixParams || o.queryParams || o.fragment);
276281
}

packages/router/src/router.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -608,25 +608,24 @@ export class Router {
608608
*/
609609
isActive(url: string | UrlTree, exact: boolean): boolean;
610610
/**
611-
* Returns whether the url is activated.
612-
* @deprecated 21.1 - Use the `isActive` function instead.
613611
* @see {@link isActive}
612+
* @deprecated 21.1 - Use the `isActive` function instead.
614613
*/
615-
isActive(url: string | UrlTree, matchOptions: IsActiveMatchOptions): boolean;
614+
isActive(url: string | UrlTree, matchOptions: Partial<IsActiveMatchOptions>): boolean;
616615
/** @internal */
617616
isActive(url: string | UrlTree, matchOptions: boolean | IsActiveMatchOptions): boolean;
618617
/**
619618
* @deprecated 21.1 - Use the `isActive` function instead.
620619
* @see {@link isActive}
621620
*/
622-
isActive(url: string | UrlTree, matchOptions: boolean | IsActiveMatchOptions): boolean {
621+
isActive(url: string | UrlTree, matchOptions: boolean | Partial<IsActiveMatchOptions>): boolean {
623622
let options: IsActiveMatchOptions;
624623
if (matchOptions === true) {
625624
options = {...exactMatchOptions};
626625
} else if (matchOptions === false) {
627626
options = {...subsetMatchOptions};
628627
} else {
629-
options = matchOptions;
628+
options = {...subsetMatchOptions, ...matchOptions};
630629
}
631630
if (isUrlTree(url)) {
632631
return containsTree(this.currentUrlTree, url, options);

packages/router/src/url_tree.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,26 @@ export const subsetMatchOptions: IsActiveMatchOptions = {
107107
*
108108
* As the router state changes, the signal will update to reflect whether the url is active.
109109
*
110+
* When using the `matchOptions` argument, any missing properties fall back to the following defaults:
111+
* - `paths`: 'subset'
112+
* - `queryParams`: 'subset'
113+
* - `matrixParams`: 'ignored'
114+
* - `fragment`: 'ignored'
115+
*
110116
* @see [Check if a URL is active](guide/routing/read-route-state#check-if-a-url-is-active)
111117
* @publicApi 21.1
112118
*/
113119
export function isActive(
114120
url: string | UrlTree,
115121
router: Router,
116-
matchOptions?: IsActiveMatchOptions,
122+
matchOptions?: Partial<IsActiveMatchOptions>,
117123
): Signal<boolean> {
118124
const urlTree = url instanceof UrlTree ? url : router.parseUrl(url);
119125
return computed(() =>
120-
containsTree(
121-
router.lastSuccessfulNavigation()?.finalUrl ?? new UrlTree(),
122-
urlTree,
123-
matchOptions ?? subsetMatchOptions,
124-
),
126+
containsTree(router.lastSuccessfulNavigation()?.finalUrl ?? new UrlTree(), urlTree, {
127+
...subsetMatchOptions,
128+
...matchOptions,
129+
}),
125130
);
126131
}
127132

packages/router/test/router_link_active.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,26 @@ describe('RouterLinkActive', () => {
2424
await fixture.whenStable();
2525
expect(Array.from(fixture.nativeElement.querySelector('a').classList)).toEqual([]);
2626
});
27+
28+
it('supports partial match options', async () => {
29+
@Component({
30+
imports: [RouterLinkActive, RouterLink],
31+
template:
32+
'<a routerLinkActive="active" [routerLinkActiveOptions]="{paths: \'exact\'}" routerLink="/abc"></a>',
33+
})
34+
class MyCmp {}
35+
36+
TestBed.configureTestingModule({providers: [provideRouter([{path: '**', children: []}])]});
37+
const fixture = TestBed.createComponent(MyCmp);
38+
fixture.autoDetectChanges();
39+
const router = TestBed.inject(Router);
40+
await router.navigateByUrl('/abc?q=1');
41+
// paths: exact matches /abc
42+
// queryParams: defaulted to subset (missing in /abc) -> match
43+
// matrixParams: defaulted to ignored -> match
44+
// fragment: defaulted to ignored -> match
45+
46+
await fixture.whenStable();
47+
expect(Array.from(fixture.nativeElement.querySelector('a').classList)).toContain('active');
48+
});
2749
});

packages/router/test/url_tree.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
containsTree,
1313
DefaultUrlSerializer,
1414
exactMatchOptions,
15+
isActive,
1516
subsetMatchOptions,
1617
} from '../src/url_tree';
1718

@@ -147,6 +148,38 @@ describe('UrlTree', () => {
147148
});
148149
});
149150

151+
describe('isActive', () => {
152+
it('should allow partial match options and use subset match options as default', () => {
153+
const router = {
154+
parseUrl: (url: string) => serializer.parse(url),
155+
lastSuccessfulNavigation: () => ({finalUrl: serializer.parse('/one/two?a=1&b=2')}),
156+
} as unknown as Router;
157+
158+
const t2 = serializer.parse('/one/two?a=1');
159+
160+
// With partial options: paths: 'exact'.
161+
// derived options should be:
162+
// paths: 'exact'
163+
// queryParams: 'subset' (default)
164+
// matrixParams: 'ignored' (default)
165+
// fragment: 'ignored' (default)
166+
167+
// This should return true because paths match exactly, and queryParams is subset (t2 is subset of t1)
168+
expect(isActive(t2, router, {paths: 'exact'})()).toBe(true);
169+
});
170+
171+
it('should use subset match options as base for other properties', () => {
172+
const router = {
173+
parseUrl: (url: string) => serializer.parse(url),
174+
lastSuccessfulNavigation: () => ({finalUrl: serializer.parse('/one/two#frag')}),
175+
} as unknown as Router;
176+
const t2 = serializer.parse('/one/two#diff');
177+
178+
// fragment is ignored by default in subsetMatchOptions
179+
expect(isActive(t2, router, {paths: 'exact'})()).toBe(true);
180+
});
181+
});
182+
150183
describe('exact = false', () => {
151184
it('should return true when containee is missing a segment', () => {
152185
const t1 = serializer.parse('/one/(two//left:three)(right:four)');

0 commit comments

Comments
 (0)