Skip to content

Commit a3c907f

Browse files
Revert "Revert "fix(router): ensure routerLinkActive updates when associated routerLinks change (angular#38349)" (angular#38511)"
This reverts commit 25ba8f4.
1 parent cd1eb76 commit a3c907f

File tree

4 files changed

+93
-15
lines changed

4 files changed

+93
-15
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ export declare class RouterEvent {
378378
url: string);
379379
}
380380

381-
export declare class RouterLink {
381+
export declare class RouterLink implements OnChanges {
382382
fragment: string;
383383
preserveFragment: boolean;
384384
/** @deprecated */ set preserveQueryParams(value: boolean);
@@ -394,6 +394,7 @@ export declare class RouterLink {
394394
};
395395
get urlTree(): UrlTree;
396396
constructor(router: Router, route: ActivatedRoute, tabIndex: string, renderer: Renderer2, el: ElementRef);
397+
ngOnChanges(changes: SimpleChanges): void;
397398
onClick(): boolean;
398399
}
399400

@@ -429,7 +430,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
429430
target: string;
430431
get urlTree(): UrlTree;
431432
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
432-
ngOnChanges(changes: {}): any;
433+
ngOnChanges(changes: SimpleChanges): any;
433434
ngOnDestroy(): any;
434435
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
435436
}

packages/router/src/directives/router_link.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {LocationStrategy} from '@angular/common';
10-
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, isDevMode, OnChanges, OnDestroy, Renderer2} from '@angular/core';
11-
import {Subscription} from 'rxjs';
10+
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, isDevMode, OnChanges, OnDestroy, Renderer2, SimpleChanges} from '@angular/core';
11+
import {Subject, Subscription} from 'rxjs';
1212

1313
import {QueryParamsHandling} from '../config';
1414
import {Event, NavigationEnd} from '../events';
@@ -115,7 +115,7 @@ import {UrlTree} from '../url_tree';
115115
* @publicApi
116116
*/
117117
@Directive({selector: ':not(a):not(area)[routerLink]'})
118-
export class RouterLink {
118+
export class RouterLink implements OnChanges {
119119
/**
120120
* Passed to {@link Router#createUrlTree Router#createUrlTree} as part of the `NavigationExtras`.
121121
* @see {@link NavigationExtras#queryParams NavigationExtras#queryParams}
@@ -167,6 +167,9 @@ export class RouterLink {
167167
private commands: any[] = [];
168168
private preserve!: boolean;
169169

170+
/** @internal */
171+
onChanges = new Subject<void>();
172+
170173
constructor(
171174
private router: Router, private route: ActivatedRoute,
172175
@Attribute('tabindex') tabIndex: string, renderer: Renderer2, el: ElementRef) {
@@ -175,6 +178,13 @@ export class RouterLink {
175178
}
176179
}
177180

181+
/** @nodoc */
182+
ngOnChanges(changes: SimpleChanges) {
183+
// This is subscribed to by `RouterLinkActive` so that it knows to update when there are changes
184+
// to the RouterLinks it's tracking.
185+
this.onChanges.next();
186+
}
187+
178188
/**
179189
* Commands to pass to {@link Router#createUrlTree Router#createUrlTree}.
180190
* - **array**: commands to pass to {@link Router#createUrlTree Router#createUrlTree}.
@@ -298,6 +308,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
298308
// TODO(issue/24571): remove '!'.
299309
@HostBinding() href!: string;
300310

311+
/** @internal */
312+
onChanges = new Subject<void>();
313+
301314
constructor(
302315
private router: Router, private route: ActivatedRoute,
303316
private locationStrategy: LocationStrategy) {
@@ -336,8 +349,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
336349
}
337350

338351
/** @nodoc */
339-
ngOnChanges(changes: {}): any {
352+
ngOnChanges(changes: SimpleChanges): any {
340353
this.updateTargetUrlAndHref();
354+
this.onChanges.next();
341355
}
342356
/** @nodoc */
343357
ngOnDestroy(): any {

packages/router/src/directives/router_link_active.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88

99
import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, Optional, QueryList, Renderer2, SimpleChanges} from '@angular/core';
10-
import {Subscription} from 'rxjs';
10+
import {from, of, Subscription} from 'rxjs';
11+
import {mergeAll} from 'rxjs/operators';
1112

1213
import {Event, NavigationEnd} from '../events';
1314
import {Router} from '../router';
@@ -79,14 +80,13 @@ import {RouterLink, RouterLinkWithHref} from './router_link';
7980
exportAs: 'routerLinkActive',
8081
})
8182
export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
82-
// TODO(issue/24571): remove '!'.
8383
@ContentChildren(RouterLink, {descendants: true}) links!: QueryList<RouterLink>;
84-
// TODO(issue/24571): remove '!'.
8584
@ContentChildren(RouterLinkWithHref, {descendants: true})
8685
linksWithHrefs!: QueryList<RouterLinkWithHref>;
8786

8887
private classes: string[] = [];
89-
private subscription: Subscription;
88+
private routerEventsSubscription: Subscription;
89+
private linkInputChangesSubscription?: Subscription;
9090
public readonly isActive: boolean = false;
9191

9292
@Input() routerLinkActiveOptions: {exact: boolean} = {exact: false};
@@ -95,7 +95,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
9595
private router: Router, private element: ElementRef, private renderer: Renderer2,
9696
private readonly cdr: ChangeDetectorRef, @Optional() private link?: RouterLink,
9797
@Optional() private linkWithHref?: RouterLinkWithHref) {
98-
this.subscription = router.events.subscribe((s: Event) => {
98+
this.routerEventsSubscription = router.events.subscribe((s: Event) => {
9999
if (s instanceof NavigationEnd) {
100100
this.update();
101101
}
@@ -104,9 +104,23 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
104104

105105
/** @nodoc */
106106
ngAfterContentInit(): void {
107-
this.links.changes.subscribe(_ => this.update());
108-
this.linksWithHrefs.changes.subscribe(_ => this.update());
109-
this.update();
107+
// `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
108+
from([this.links.changes, this.linksWithHrefs.changes, of(null)])
109+
.pipe(mergeAll())
110+
.subscribe(_ => {
111+
this.update();
112+
this.subscribeToEachLinkOnChanges();
113+
});
114+
}
115+
116+
private subscribeToEachLinkOnChanges() {
117+
this.linkInputChangesSubscription?.unsubscribe();
118+
const allLinkChanges =
119+
[...this.links.toArray(), ...this.linksWithHrefs.toArray(), this.link, this.linkWithHref]
120+
.filter((link): link is RouterLink|RouterLinkWithHref => !!link)
121+
.map(link => link.onChanges);
122+
this.linkInputChangesSubscription =
123+
from(allLinkChanges).pipe(mergeAll()).subscribe(() => this.update());
110124
}
111125

112126
@Input()
@@ -121,7 +135,8 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
121135
}
122136
/** @nodoc */
123137
ngOnDestroy(): void {
124-
this.subscription.unsubscribe();
138+
this.routerEventsSubscription.unsubscribe();
139+
this.linkInputChangesSubscription?.unsubscribe();
125140
}
126141

127142
private update(): void {

packages/router/test/regression_integration.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,54 @@ import {RouterTestingModule} from '@angular/router/testing';
1414

1515
describe('Integration', () => {
1616
describe('routerLinkActive', () => {
17+
it('should update when the associated routerLinks change - #18469', fakeAsync(() => {
18+
@Component({
19+
template: `
20+
<a id="first-link" [routerLink]="[firstLink]" routerLinkActive="active">{{firstLink}}</a>
21+
<div id="second-link" routerLinkActive="active">
22+
<a [routerLink]="[secondLink]">{{secondLink}}</a>
23+
</div>
24+
`,
25+
})
26+
class LinkComponent {
27+
firstLink = 'link-a';
28+
secondLink = 'link-b';
29+
30+
changeLinks(): void {
31+
const temp = this.secondLink;
32+
this.secondLink = this.firstLink;
33+
this.firstLink = temp;
34+
}
35+
}
36+
37+
@Component({template: 'simple'})
38+
class SimpleCmp {
39+
}
40+
41+
TestBed.configureTestingModule({
42+
imports: [RouterTestingModule.withRoutes(
43+
[{path: 'link-a', component: SimpleCmp}, {path: 'link-b', component: SimpleCmp}])],
44+
declarations: [LinkComponent, SimpleCmp]
45+
});
46+
47+
const router: Router = TestBed.inject(Router);
48+
const fixture = createRoot(router, LinkComponent);
49+
const firstLink = fixture.debugElement.query(p => p.nativeElement.id === 'first-link');
50+
const secondLink = fixture.debugElement.query(p => p.nativeElement.id === 'second-link');
51+
router.navigateByUrl('/link-a');
52+
advance(fixture);
53+
54+
expect(firstLink.nativeElement.classList).toContain('active');
55+
expect(secondLink.nativeElement.classList).not.toContain('active');
56+
57+
fixture.componentInstance.changeLinks();
58+
fixture.detectChanges();
59+
advance(fixture);
60+
61+
expect(firstLink.nativeElement.classList).not.toContain('active');
62+
expect(secondLink.nativeElement.classList).toContain('active');
63+
}));
64+
1765
it('should not cause infinite loops in the change detection - #15825', fakeAsync(() => {
1866
@Component({selector: 'simple', template: 'simple'})
1967
class SimpleCmp {

0 commit comments

Comments
 (0)