Skip to content

Commit a692802

Browse files
committed
feat: fetch routerLink from latest upstream
1 parent 02c218f commit a692802

File tree

2 files changed

+213
-76
lines changed

2 files changed

+213
-76
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,250 @@
1-
import { AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer2 } from '@angular/core';
2-
import { Subscription } from 'rxjs';
3-
4-
import { NavigationEnd, Router, UrlTree } from '@angular/router';
5-
import { containsTree } from './private-imports/router-url-tree';
6-
1+
import {
2+
AfterContentInit,
3+
ChangeDetectorRef,
4+
ContentChildren,
5+
Directive,
6+
ElementRef,
7+
EventEmitter,
8+
Input,
9+
OnChanges,
10+
OnDestroy,
11+
Optional,
12+
Output,
13+
QueryList,
14+
Renderer2,
15+
SimpleChanges,
16+
} from '@angular/core';
17+
import { Event, IsActiveMatchOptions, NavigationEnd, Router } from '@angular/router';
18+
import { from, of, Subscription } from 'rxjs';
19+
import { mergeAll } from 'rxjs/operators';
720
import { NSRouterLink } from './ns-router-link';
821

922
/**
10-
* The NSRouterLinkActive directive lets you add a CSS class to an element when the link"s route
11-
* becomes active.
1223
*
13-
* Consider the following example:
24+
* @description
1425
*
15-
* ```
16-
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="active-link">Bob</a>
26+
* Tracks whether the linked route of an element is currently active, and allows you
27+
* to specify one or more CSS classes to add to the element when the linked route
28+
* is active.
29+
*
30+
* Use this directive to create a visual distinction for elements associated with an active route.
31+
* For example, the following code highlights the word "Bob" when the router
32+
* activates the associated route:
33+
*
34+
* ```html
35+
* <a nsRouterLink="/user/bob" nsRouterLinkActive="active-link">Bob</a>
1736
* ```
1837
*
19-
* When the url is either "/user" or "/user/bob", the active-link class will
20-
* be added to the component. If the url changes, the class will be removed.
38+
* Whenever the URL is either '/user' or '/user/bob', the "active-link" class is
39+
* added to the anchor tag. If the URL changes, the class is removed.
2140
*
22-
* You can set more than one class, as follows:
41+
* You can set more than one class using a space-separated string or an array.
42+
* For example:
2343
*
24-
* ```
25-
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="class1 class2">Bob</a>
26-
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="["class1", "class2"]">Bob</a>
44+
* ```html
45+
* <a nsRouterLink="/user/bob" nsRouterLinkActive="class1 class2">Bob</a>
46+
* <a nsRouterLink="/user/bob" [nsRouterLinkActive]="['class1', 'class2']">Bob</a>
2747
* ```
2848
*
29-
* You can configure NSRouterLinkActive by passing `exact: true`. This will add the
30-
* classes only when the url matches the link exactly.
49+
* To add the classes only when the URL matches the link exactly, add the option `exact: true`:
3150
*
32-
* ```
33-
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="active-link"
34-
* [nsRouterLinkActiveOptions]="{exact: true}">Bob</a>
51+
* ```html
52+
* <a nsRouterLink="/user/bob" nsRouterLinkActive="active-link" [nsRouterLinkActiveOptions]="{exact:
53+
* true}">Bob</a>
3554
* ```
3655
*
37-
* Finally, you can apply the NSRouterLinkActive directive to an ancestor of a RouterLink.
56+
* To directly check the `isActive` status of the link, assign the `NsRouterLinkActive`
57+
* instance to a template variable.
58+
* For example, the following checks the status without assigning any CSS classes:
3859
*
60+
* ```html
61+
* <a nsRouterLink="/user/bob" nsRouterLinkActive #rla="nsRouterLinkActive">
62+
* Bob {{ rla.isActive ? '(already open)' : ''}}
63+
* </a>
3964
* ```
40-
* <div [nsRouterLinkActive]="active-link" [nsRouterLinkActiveOptions]="{exact: true}">
41-
* <a [nsRouterLink]="/user/jim">Jim</a>
42-
* <a [nsRouterLink]="/user/bob">Bob</a>
65+
*
66+
* You can apply the `NsRouterLinkActive` directive to an ancestor of linked elements.
67+
* For example, the following sets the active-link class on the `<div>` parent tag
68+
* when the URL is either '/user/jim' or '/user/bob'.
69+
*
70+
* ```html
71+
* <div nsRouterLinkActive="active-link" [nsRouterLinkActiveOptions]="{exact: true}">
72+
* <a nsRouterLink="/user/jim">Jim</a>
73+
* <a nsRouterLink="/user/bob">Bob</a>
4374
* </div>
4475
* ```
4576
*
46-
* This will set the active-link class on the div tag if the url is either "/user/jim" or
47-
* "/user/bob".
77+
* The `NsRouterLinkActive` directive can also be used to set the aria-current attribute
78+
* to provide an alternative distinction for active elements to visually impaired users.
79+
*
80+
* For example, the following code adds the 'active' class to the Home Page link when it is
81+
* indeed active and in such case also sets its aria-current attribute to 'page':
82+
*
83+
* ```html
84+
* <a nsRouterLink="/" nsRouterLinkActive="active" ariaCurrentWhenActive="page">Home Page</a>
85+
* ```
86+
*
87+
* @ngModule RouterModule
4888
*
49-
* @stable
89+
* @publicApi
5090
*/
5191
@Directive({
5292
selector: '[nsRouterLinkActive]',
53-
exportAs: 'routerLinkActive',
54-
standalone: true,
93+
exportAs: 'nsRouterLinkActive',
5594
})
56-
export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
57-
// tslint:disable-line:max-line-length directive-class-suffix
58-
@ContentChildren(NSRouterLink) links: QueryList<NSRouterLink>;
95+
export class NsRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
96+
@ContentChildren(NSRouterLink, { descendants: true }) links!: QueryList<NSRouterLink>;
5997

6098
private classes: string[] = [];
61-
private subscription: Subscription;
62-
private active = false;
99+
private routerEventsSubscription: Subscription;
100+
private linkInputChangesSubscription?: Subscription;
101+
private _isActive = false;
102+
103+
get isActive() {
104+
return this._isActive;
105+
}
106+
107+
/**
108+
* Options to configure how to determine if the router link is active.
109+
*
110+
* These options are passed to the `Router.isActive()` function.
111+
*
112+
* @see {@link Router#isActive}
113+
*/
114+
@Input() nsRouterLinkActiveOptions: { exact: boolean } | IsActiveMatchOptions = { exact: false };
63115

64-
@Input() nsRouterLinkActiveOptions: { exact: boolean } = { exact: false };
116+
/**
117+
* Aria-current attribute to apply when the router link is active.
118+
*
119+
* Possible values: `'page'` | `'step'` | `'location'` | `'date'` | `'time'` | `true` | `false`.
120+
*
121+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current}
122+
*/
123+
@Input() ariaCurrentWhenActive?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false;
65124

66-
constructor(private router: Router, private element: ElementRef, private renderer: Renderer2) {
67-
this.subscription = router.events.subscribe((s) => {
125+
/**
126+
*
127+
* You can use the output `isActiveChange` to get notified each time the link becomes
128+
* active or inactive.
129+
*
130+
* Emits:
131+
* true -> Route is active
132+
* false -> Route is inactive
133+
*
134+
* ```html
135+
* <a
136+
* nsRouterLink="/user/bob"
137+
* nsRouterLinkActive="active-link"
138+
* (isActiveChange)="this.onNsRouterLinkActive($event)">Bob</a>
139+
* ```
140+
*/
141+
@Output() readonly isActiveChange: EventEmitter<boolean> = new EventEmitter();
142+
143+
constructor(
144+
private router: Router,
145+
private element: ElementRef,
146+
private renderer: Renderer2,
147+
private readonly cdr: ChangeDetectorRef,
148+
@Optional() private link?: NSRouterLink,
149+
) {
150+
this.routerEventsSubscription = router.events.subscribe((s: Event) => {
68151
if (s instanceof NavigationEnd) {
69152
this.update();
70153
}
71154
});
72155
}
73156

74-
get isActive(): boolean {
75-
return this.active;
157+
/** @nodoc */
158+
ngAfterContentInit(): void {
159+
// `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
160+
of(this.links.changes, of(null))
161+
.pipe(mergeAll())
162+
.subscribe((_) => {
163+
this.update();
164+
this.subscribeToEachLinkOnChanges();
165+
});
76166
}
77167

78-
ngAfterContentInit(): void {
79-
this.links.changes.subscribe(() => this.update());
80-
this.update();
168+
private subscribeToEachLinkOnChanges() {
169+
this.linkInputChangesSubscription?.unsubscribe();
170+
const allLinkChanges = [...this.links.toArray(), this.link]
171+
.filter((link): link is NSRouterLink => !!link)
172+
.map((link) => link.onChanges);
173+
this.linkInputChangesSubscription = from(allLinkChanges)
174+
.pipe(mergeAll())
175+
.subscribe((link) => {
176+
if (this._isActive !== this.isLinkActive(this.router)(link)) {
177+
this.update();
178+
}
179+
});
81180
}
82181

83182
@Input()
84183
set nsRouterLinkActive(data: string[] | string) {
85-
if (Array.isArray(data)) {
86-
this.classes = <any>data;
87-
} else {
88-
this.classes = data.split(' ');
89-
}
184+
const classes = Array.isArray(data) ? data : data.split(' ');
185+
this.classes = classes.filter((c) => !!c);
90186
}
91187

92-
ngOnChanges() {
188+
/** @nodoc */
189+
ngOnChanges(changes: SimpleChanges): void {
93190
this.update();
94191
}
95-
ngOnDestroy() {
96-
this.subscription.unsubscribe();
192+
/** @nodoc */
193+
ngOnDestroy(): void {
194+
this.routerEventsSubscription.unsubscribe();
195+
this.linkInputChangesSubscription?.unsubscribe();
97196
}
98197

99198
private update(): void {
100-
if (!this.links) {
101-
return;
102-
}
103-
const hasActiveLinks = this.hasActiveLinks();
104-
// react only when status has changed to prevent unnecessary dom updates
105-
if (this.active !== hasActiveLinks) {
106-
const currentUrlTree = this.router.parseUrl(this.router.url);
107-
const isActiveLinks = this.reduceList(currentUrlTree, this.links);
199+
if (!this.links || !this.router.navigated) return;
200+
201+
queueMicrotask(() => {
202+
const hasActiveLinks = this.hasActiveLinks();
108203
this.classes.forEach((c) => {
109-
if (isActiveLinks) {
204+
if (hasActiveLinks) {
110205
this.renderer.addClass(this.element.nativeElement, c);
111206
} else {
112207
this.renderer.removeClass(this.element.nativeElement, c);
113208
}
114209
});
115-
}
116-
Promise.resolve(hasActiveLinks).then((active) => (this.active = active));
117-
}
210+
// we don't have aria in nativescript
211+
// if (hasActiveLinks && this.ariaCurrentWhenActive !== undefined) {
212+
// this.renderer.setAttribute(this.element.nativeElement, 'aria-current', this.ariaCurrentWhenActive.toString());
213+
// } else {
214+
// this.renderer.removeAttribute(this.element.nativeElement, 'aria-current');
215+
// }
118216

119-
private reduceList(currentUrlTree: UrlTree, q: QueryList<any>): boolean {
120-
return q.reduce((res: boolean, link: NSRouterLink) => {
121-
return res || containsTree(currentUrlTree, link.urlTree, this.nsRouterLinkActiveOptions.exact);
122-
}, false);
217+
// Only emit change if the active state changed.
218+
if (this._isActive !== hasActiveLinks) {
219+
this._isActive = hasActiveLinks;
220+
this.cdr.markForCheck();
221+
// Emit on isActiveChange after classes are updated
222+
this.isActiveChange.emit(hasActiveLinks);
223+
}
224+
});
123225
}
124226

125227
private isLinkActive(router: Router): (link: NSRouterLink) => boolean {
126-
return (link: NSRouterLink) => router.isActive(link.urlTree, this.nsRouterLinkActiveOptions.exact);
228+
const options: boolean | IsActiveMatchOptions = isActiveMatchOptions(this.nsRouterLinkActiveOptions)
229+
? this.nsRouterLinkActiveOptions
230+
: // While the types should disallow `undefined` here, it's possible without strict inputs
231+
this.nsRouterLinkActiveOptions.exact || false;
232+
return (link: NSRouterLink) => {
233+
const urlTree = link.urlTree;
234+
// hardcoding the "as" there to make TS happy, but this function has overloads for both boolean and IsActiveMatchOptions
235+
return urlTree ? router.isActive(urlTree, options as IsActiveMatchOptions) : false;
236+
};
127237
}
128238

129239
private hasActiveLinks(): boolean {
130-
return this.links.some(this.isLinkActive(this.router));
240+
const isActiveCheckFn = this.isLinkActive(this.router);
241+
return (this.link && isActiveCheckFn(this.link)) || this.links.some(isActiveCheckFn);
131242
}
132243
}
244+
245+
/**
246+
* Use instead of `'paths' in options` to be compatible with property renaming
247+
*/
248+
function isActiveMatchOptions(options: { exact: boolean } | IsActiveMatchOptions): options is IsActiveMatchOptions {
249+
return !!(options as IsActiveMatchOptions).paths;
250+
}

packages/angular/src/lib/legacy/router/ns-router-link.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Directive, Input, ElementRef, NgZone, AfterViewInit } from '@angular/core';
1+
import { Directive, Input, ElementRef, NgZone, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core';
22
import { NavigationExtras } from '@angular/router';
33
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
44
import { NavigationTransition } from '@nativescript/core';
55
import { NativeScriptDebug } from '../../trace';
66
import { RouterExtensions } from './router-extensions';
77
import { NavigationOptions } from './ns-location-utils';
8+
import { Subject } from 'rxjs';
89

910
// Copied from "@angular/router/src/config"
1011
export type QueryParamsHandling = 'merge' | 'preserve' | '';
@@ -33,11 +34,11 @@ export type QueryParamsHandling = 'merge' | 'preserve' | '';
3334
* instead look in the current component"s children for the route.
3435
* And if the segment begins with `../`, the router will go up one level.
3536
*/
36-
@Directive({
37-
selector: '[nsRouterLink]',
37+
@Directive({
38+
selector: '[nsRouterLink]',
3839
standalone: true,
3940
})
40-
export class NSRouterLink implements AfterViewInit {
41+
export class NSRouterLink implements OnChanges, AfterViewInit {
4142
// tslint:disable-line:directive-class-suffix
4243
@Input() target: string;
4344
@Input() queryParams: { [k: string]: any };
@@ -55,7 +56,20 @@ export class NSRouterLink implements AfterViewInit {
5556

5657
private commands: any[] = [];
5758

58-
constructor(private ngZone: NgZone, private router: Router, private navigator: RouterExtensions, private route: ActivatedRoute, private el: ElementRef) {}
59+
/** @internal */
60+
onChanges = new Subject<NSRouterLink>();
61+
62+
constructor(
63+
private ngZone: NgZone,
64+
private router: Router,
65+
private navigator: RouterExtensions,
66+
private route: ActivatedRoute,
67+
private el: ElementRef,
68+
) {}
69+
70+
ngOnChanges(changes?: SimpleChanges): void {
71+
this.onChanges.next(this);
72+
}
5973

6074
ngAfterViewInit() {
6175
this.el.nativeElement.on('tap', () => {
@@ -76,7 +90,12 @@ export class NSRouterLink implements AfterViewInit {
7690

7791
onTap() {
7892
if (NativeScriptDebug.isLogEnabled()) {
79-
NativeScriptDebug.routerLog(`nsRouterLink.tapped: ${this.commands} ` + `clear: ${this.clearHistory} ` + `transition: ${JSON.stringify(this.pageTransition)} ` + `duration: ${this.pageTransitionDuration}`);
93+
NativeScriptDebug.routerLog(
94+
`nsRouterLink.tapped: ${this.commands} ` +
95+
`clear: ${this.clearHistory} ` +
96+
`transition: ${JSON.stringify(this.pageTransition)} ` +
97+
`duration: ${this.pageTransitionDuration}`,
98+
);
8099
}
81100

82101
const extras = this.getExtras();

0 commit comments

Comments
 (0)